From c0a552a1cff2ef4306eec1d2353659aefbd4e28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=94=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=81?= Date: Fri, 30 Apr 2021 09:17:11 +0300 Subject: [PATCH] Fixed force parameter. Added new worker ipc, some pydantic response and requests schemas and server api description. --- calculate/commands/commands.py | 4 +- calculate/parameters/parameters.py | 6 +- calculate/scripts/scripts.py | 20 +- .../{models/database.py => database/db.py} | 0 .../server/{models => database}/users.py | 0 .../server/{models => database}/workers.py | 0 calculate/server/interfaces.txt | 830 ++++++++++ calculate/server/ipc_draft/async_client.py | 165 ++ calculate/server/ipc_draft/cl-command-0 | 1 + calculate/server/ipc_draft/cl-command-1 | 1 + calculate/server/ipc_draft/cl-command-2 | 1 + calculate/server/ipc_draft/cl-command-3 | 1 + calculate/server/ipc_draft/client.py | 183 +++ calculate/server/ipc_draft/commands.py | 77 + calculate/server/ipc_draft/daemon.py | 121 ++ calculate/server/ipc_draft/example/ipc.py | 143 ++ calculate/server/ipc_draft/interfaces.txt | 830 ++++++++++ calculate/server/ipc_draft/io_module.py | 100 ++ calculate/server/ipc_draft/schemas.py | 202 +++ calculate/server/ipc_draft/scripts.py | 55 + calculate/server/ipc_draft/server.py | 282 ++++ calculate/server/ipc_draft/utils.py | 199 +++ calculate/server/ipc_draft/worker.py | 413 +++++ calculate/server/ipc_draft/worker_old.py | 428 ++++++ calculate/server/routers/commands.py | 104 +- .../schemas/{workers.py => commands.py} | 0 calculate/server/schemas/requests.py | 22 + calculate/server/schemas/responses.py | 180 +++ calculate/server/server.py | 71 +- calculate/server/server_data.py | 129 +- calculate/server/utils/auth.py | 2 +- calculate/server/utils/daemon.py | 5 + calculate/server/utils/responses.py | 122 ++ calculate/server/utils/users.py | 39 +- calculate/server/utils/workers.py | 622 ++++++-- .../templates/format/backgrounds_format.py | 4 +- calculate/templates/format/base_format.py | 2 + calculate/templates/format/bind_format.py | 22 +- calculate/templates/format/dovecot_format.py | 2 +- calculate/templates/format/kde_format.py | 17 +- calculate/templates/format/kernel_format.py | 15 +- calculate/templates/format/patch_format.py | 3 +- calculate/templates/format/procmail_format.py | 16 +- calculate/templates/format/samba_format.py | 2 - calculate/templates/template_engine.py | 121 +- calculate/templates/template_processor.py | 145 +- calculate/utils/calculateini.py | 89 -- calculate/utils/contents_template | 14 - calculate/utils/gentoo.py | 13 +- calculate/utils/ldap.py | 0 calculate/utils/package.py | 98 +- calculate/utils/system.py | 5 +- calculate/utils/tools.py | 3 + calculate/variables/datavars.py | 26 +- calculate/variables/loader.py | 120 +- calculate/vars/main/__init__.py | 6 +- calculate/vars/main/cl/__init__.py | 1 + calculate/vars/main/os/__init__.py | 5 + output | 1339 ----------------- pytest.ini | 2 + run_server.py | 9 +- run_templates.py | 3 + tests/parameters/test_parameters.py | 14 +- tests/scripts/test_scripts.py | 62 +- tests/server/test_responses.py | 232 +++ tests/server/test_server.py | 48 +- tests/templates/test_template_executor.py | 2 +- tests/templates/test_template_filters.py | 2 + .../test-category/test-package-1.0/CONTENTS | 1 + tests/variables/test_calculateini.py | 1 + 70 files changed, 5733 insertions(+), 2069 deletions(-) rename calculate/server/{models/database.py => database/db.py} (100%) rename calculate/server/{models => database}/users.py (100%) rename calculate/server/{models => database}/workers.py (100%) create mode 100644 calculate/server/interfaces.txt create mode 100755 calculate/server/ipc_draft/async_client.py create mode 120000 calculate/server/ipc_draft/cl-command-0 create mode 120000 calculate/server/ipc_draft/cl-command-1 create mode 120000 calculate/server/ipc_draft/cl-command-2 create mode 120000 calculate/server/ipc_draft/cl-command-3 create mode 100755 calculate/server/ipc_draft/client.py create mode 100644 calculate/server/ipc_draft/commands.py create mode 100644 calculate/server/ipc_draft/daemon.py create mode 100644 calculate/server/ipc_draft/example/ipc.py create mode 100644 calculate/server/ipc_draft/interfaces.txt create mode 100644 calculate/server/ipc_draft/io_module.py create mode 100644 calculate/server/ipc_draft/schemas.py create mode 100644 calculate/server/ipc_draft/scripts.py create mode 100644 calculate/server/ipc_draft/server.py create mode 100644 calculate/server/ipc_draft/utils.py create mode 100644 calculate/server/ipc_draft/worker.py create mode 100644 calculate/server/ipc_draft/worker_old.py rename calculate/server/schemas/{workers.py => commands.py} (100%) create mode 100644 calculate/server/schemas/requests.py create mode 100644 calculate/server/schemas/responses.py create mode 100644 calculate/server/utils/daemon.py create mode 100644 calculate/server/utils/responses.py delete mode 100644 calculate/utils/calculateini.py delete mode 100644 calculate/utils/contents_template delete mode 100644 calculate/utils/ldap.py delete mode 100644 output create mode 100644 tests/server/test_responses.py diff --git a/calculate/commands/commands.py b/calculate/commands/commands.py index 563008f..3873750 100644 --- a/calculate/commands/commands.py +++ b/calculate/commands/commands.py @@ -42,11 +42,11 @@ class Command: def __init__(self, command_id: str = '', category: str = '', title: str = '', + command: str = '', script: Union[Callable, Script, None] = None, args: Union[tuple, list] = tuple(), parameters: Union[List[BaseParameter], None] = None, namespace: Union[str, NamespaceNode, None] = None, - command: str = '', gui: bool = False, icon: Union[str, List[str]] = '', setvars: dict = {}, @@ -176,7 +176,7 @@ class CommandRunner: def __init__(self, command: Command, datavars: Union[Datavars, NamespaceNode], output: IOModule): - '''Класс инкапулирующий все данные о команде, а также необходимые для + '''Класс инкапcулирующий все данные о команде, а также необходимые для нее параметры, модуль ввода-вывода, переменные и прочее. Предназначен для конфигурации команды и запуска ее в воркере.''' self._command = command diff --git a/calculate/parameters/parameters.py b/calculate/parameters/parameters.py index c2be990..bb43b9d 100644 --- a/calculate/parameters/parameters.py +++ b/calculate/parameters/parameters.py @@ -232,7 +232,7 @@ class Table(ParameterType): def process_value(self, value: TableValue, parameter): if isinstance(value, TableValue): # Если родитель не установлен -- значит параметр был только что - # инициализирован.его, и проводим полную проверку + # инициализирован. Устанавливаем его, и проводим полную проверку # инициализированных значений таблицы. if value._parent is None: value._parent = parameter @@ -270,7 +270,7 @@ class Password(ParameterType): class Description: '''Класс, содержащий описание параметра и способ его отображения.''' - # словарь со способами отображения параметра в графическом интерфейсе. + # Словарь со способами отображения параметра в графическом интерфейсе. representations = {} def __init__(self, short='', full='', usage=''): @@ -434,7 +434,7 @@ class BaseParameter: else: return None, None - def to_set(self, *variables): + def set_to(self, *variables): '''Метод для добавления переменных, которым будет установлено значение параметра.''' self._subscribers = variables diff --git a/calculate/scripts/scripts.py b/calculate/scripts/scripts.py index ccc7ea2..72231af 100644 --- a/calculate/scripts/scripts.py +++ b/calculate/scripts/scripts.py @@ -2,7 +2,7 @@ # import re import inspect -from typing import Callable, Any, Union, List, Generator +from typing import Callable, Any, Union, List, Generator, Optional from calculate.templates.template_processor import DirectoryProcessor from calculate.variables.datavars import ( DependenceAPI, @@ -46,18 +46,18 @@ class Script: в себе. Создает экземпляры лаунчеров.''' def __init__(self, script_id: str, args: List[Any] = [], - success_message: Union[str, None] = None, - failed_message: Union[str, None] = None, - interrupt_message: Union[str, None] = None): + success_message: Optional[str] = None, + failed_message: Optional[str] = None, + interrupt_message: Optional[str] = None): self._id: str = script_id self.__items: List[Union['Task', 'Block', 'Handler', 'Run']] = None self.__tasks_is_set: bool = False self.__args: list = args - self.__success_message: Union[str, None] = success_message - self.__failed_message: Union[str, None] = failed_message - self.__interrupt_message: Union[str, None] = interrupt_message + self.__success_message: Optional[str] = success_message + self.__failed_message: Optional[str] = failed_message + self.__interrupt_message: Optional[str] = interrupt_message @property def id(self) -> str: @@ -198,7 +198,7 @@ class ScriptLauncher: return scripts - def __call__(self, *args): + def __call__(self, *args) -> None: '''Метод для запуска скрипта с аргументами.''' if len(args) < len(self._script.args): raise ScriptError( @@ -219,7 +219,7 @@ class ScriptLauncher: def _get_args_values(self, args: List[Any], datavars: Union[Datavars, NamespaceNode], - namespace: NamespaceNode): + namespace: NamespaceNode) -> list: '''Метод для получения значений аргументов.''' args_values = [] for argument in args: @@ -235,7 +235,7 @@ class ScriptLauncher: return args_values -class RunTemplate: +class RunTemplates: '''Класс запускателя наложения шаблонов.''' def __init__(self, id: str = '', action: str = '', diff --git a/calculate/server/models/database.py b/calculate/server/database/db.py similarity index 100% rename from calculate/server/models/database.py rename to calculate/server/database/db.py diff --git a/calculate/server/models/users.py b/calculate/server/database/users.py similarity index 100% rename from calculate/server/models/users.py rename to calculate/server/database/users.py diff --git a/calculate/server/models/workers.py b/calculate/server/database/workers.py similarity index 100% rename from calculate/server/models/workers.py rename to calculate/server/database/workers.py diff --git a/calculate/server/interfaces.txt b/calculate/server/interfaces.txt new file mode 100644 index 0000000..fced117 --- /dev/null +++ b/calculate/server/interfaces.txt @@ -0,0 +1,830 @@ +Вопросы +------- +1. >> Что является ресурсом? + * commands -- информация о командах, их список, группы, описания и тд. + * workers(?) -- воркеры, исполняющие команды. Сомнительно. + Возможно есть смысл скрыть реализацию, т.е. воркеров, под некоторыми + другими сущностями. Например, execution и configuration. + workers -- мб оставить как дополнительный низкоуровневый интерфейс. + << Ресурсы: commands, configuration, execution, workers (low_level) + +2. >> Что делать с демонами-воркерами, обнаруженными при инциализации сервера, + но не зарегистрированными в БД? + << Убиваем их и удаляем PID-файлы. + +3. >> Что делать с демонами-воркерами, для которых найдены PID-файлы, по PID + есть информация в БД, но WID из имени PID-файла не совпадает с WID в БД. + << Убиваем воркера, удаляем PID-файл и запись в БД. + +4. >> Стоит ли убрать разделение команд и их id? + << Не стоит. + +5. >> Нужна ли возможность сброса текущих параметров конфигурации? + << Да. + + +Взаимодействие с воркером с точки зрения сервера +------------------------------------------------ +1. Сервер получает от клиента запрос, POST /configs/{command_id}. Далее: + + * Сервер создает объект воркера команды и запускает с его помощью + демона-рабочего, оставляя его в режиме ожидания конфигурационных данных. + + * После создания объекта воркера в БД сохраняется основная информация + необходимая для воссоздания объекта воркера в новом процессе сервера. Такая + ситуация может возникнуть при переходе на новую версию сервера: + + - int wid -- идентификатор воркера; + + - str socket -- путь к сокету для чтения данных воркера; + + - int daemon_pid -- PID демона-воркера для взаимодействия с ним. + + * В ответ на запрос сервер получает список доступных ресурсов, включающих + в себя в том числе действия воркера, а также URI этих ресурсов и + доступные для этого URI http-методы. На данном этапе: + + - configure (patch) -- изменить некоторые параметры конфигурации; + + - cancel (delete) -- закончить конфигурацию без продолжения выполнения. + По сути удаляет данную конфигурацию как ресурс; + + - execute (post) -- запустить выполнение команды с дефолтной + конфигурацией. По сути удаляет конфигурацию как ресурс и создает + новый ресурс -- выполнение. + +2. Сервер получает от клиента запрос PATCH /configs/{WID}/parameters, + содержащий в теле конфигурационные данные. Далее: + + * Формируется конфигурационное сообщение (1a), содержащее в себе список + объектов, описывающих один или несколько параметров команды, которое + затем отправляется воркеру. + + * Oжидается ответ от воркера (1a) с информацией о результатах обработки + параметров, найденных ошибках и т.д, который пересылается клиенту. + + * Конфигурационный цикл повторяется. + + Возможно стоит добавить на стороне сервера таймаут при получении ответа от + воркера. + +3. Сервер получает от клиента запрос POST /execs/{WID}, указывающий + закончить конфигурацию команды и начать выполнение. + Далее: + + * Формируется сообщение воркеру (3a), сообщающее уже ему об окончании цикла + конфигурации. + + * Если есть неисправленные ошибки, воркер отвечает на него сообщением (4b), + содержащим оповещение о том, что он заканчивает работу; + Если ошибок нет, то сообщением (4a), оповещающим об успешном запуске + скрипта команды. + + * В случае, когда обработка параметров прошла с ошибками -- сразу после + выдачи сообщения об ошибках конфигурации, воркер отправляет сообщение + об окончании своей работы, содержащее список ошибок (9b) и (?) ждет + подтверждение от сервера (?). + +4. Сервер получает от клиента запрос GET /execs/{WID}/output на + получение имеющихся выходных данных воркера. Далее: + + * Cервер читает все, что есть на сокете воркера. Сообщения (5a, 5b или 6a) + на сокете разделены нулевым символом '\0'. Собирается список полных + сообщений на сокете, формируется сообщение, содержащее этот список, + которое отправляется клиенту. + +5. Сервер получает от клиента запрос PATCH /execs/{WID}/input на передачу + воркеру ввода, который был запрошен последним в одном из сообщений ранее + переданных клиенту через сервер. Далее: + + * Сервер формирует сообщение (7a) воркеру с данными ввода и отправляет его + воркеру. Если данных нет, что означает, что ввод был сброшен + пользователем, воркер отправляет серверу сообщение (7b) об некорректном + вводе, а метод input объекта ввода/вывода кидает в скрипте специальное + исключение. + + Возможно, необходимо убеждаться в том, что данные успешно получены воркером. + Воркер должен ответить сообщением (8a) об успешном получении данных. + + - Если данное сообщение не получено в течение некоторого времени + (таймаута) -- выполняем некоторые действия по диагностике и устранению + последствий ошибки, затем отправляем клиенту сообщение, сообщающее о том, + что воркер закончил работу, и содержащее последние сообщения, полученные + от воркера, если таковые имеются. + + - Если сообщение об успешном вводе в воркер получено -- отправляем + соответствующее сообщение клиенту. + +6. Работа воркера закончилась в результате успешного выполнения команды, + или было прервано клиентом командой stop, или по причине ошибок, возникших + при выполнении скриптов команды. Далее: + + * Воркер отправляет на сервер одно из двух специальных сообщений, + оповещающих об окончании работы: + + - Содержащее также информацию об ошибках (9b), если таковые + были, неотправленных по каким-то причинам сообщениях (?) или о факте + выполнения команды stop. + + - Содержащие только объявление об окончании выполнения команды, то есть + состояния run. + + В зависимости от того, возможно ли чтение из сокета, когда процесс воркера + уже закрыт сервер будет или не будет отправлять сообщение разрешающее + воркеру прекратить работу, но клиент его видеть не должен. По факту + получения конечного сообщения и, возможно, данных вместе с ним, клиенту + будет присылаться специальное сообщение. + + +Контекст воркера +---------------- +worker_state -- состояние воркера; +socket_context -- контекст чтения сокета воркера; +worker_output -- вывод воркера перед его выключением сервера. + + +Что делать +---------- +[*] Проверить возможность чтения сервером данных из сокета уже закрытого + воркера. Если такой возможности нет, предусмотреть обработку завершения и + успешной, и некорректной работы воркера с передачей всех необходимых + сообщений. Сделать это внутри воркера. + Итог: чтение из сокета убитого демона возможно. + +[ ] Добавить в воркер цикл конфигурации или его тестовый вариант; + +[*] Убрать на стороне интерфейса клиента лишний шаг создания объекта воркера; + +[*] Убрать self._runner_process и self._pid_file, они в таком случае не нужны; + +[*] PID-файл демона удаляем сразу после чтения; + +[ ] Реализовать описанный ниже протокол взаимодействия сервера с воркером; + +[ ] Добавить в метод kill воркера чтение всего содержимого сокета перед его + удалением. + +[ ] Разработать HAL-формат сообщений об ошибках вместо HTTPException. + +[*] Сделать метод read воркера асинхронным. + +[ ] На случай если воркер убит через SIGKILL -- поскольку обработку + такого завершения работы воркера в самом воркере организовать невозможно, + добавить дополнительную проверку is_alive воркера и предусмотреть обработку + этой ситуации. + +[*] Добавить главный блок try-catch-finally в воркер. В раздел finally добавить + отправку сообщения об окончании работы воркера. В зависимости от + возможности читать данный из сокета законченного процесса воркера, ждать + или не ждать ответ сервера по поводу окончания работы воркера; + +[*] Добавить предварительно деление воркеров по состояниям, вероятно добавить + WorkersManager; + +[ ] Добавить состояния воркера: + - config -- воркер в состоянии конфигурации, Т.е. ждёт значения + параметров или собщение об окончании конфигурации; + - exec -- воркер в процессе выполнения команды; + - input -- воркер в процессе ожидания ввода пользователя; + - finish -- воркер в состоянии завершения некоторого состояния. + +[ ] Реализовать предварительно REST API в соответствии с наработками; + +[*] Решить, какое значение должны иметь заголовки Content-Type и Accept: + - application/json; + - application/hal+json; + - application/vnd.cphl+json; + - application/vnd.api+json. + Подумать над версионированием API; + +[ ] Добавить в ответы сервера обозначения о кэшируемости и некэшируемости, + проработать кэширование в целом, определить, что должно кэшироваться, а что + нет; + +[ ] Проработать механизм остановки взаимодействия сервера с клиентом и + возвращения к нему после переподключения клиента, перезагрузки сервера, а + также при этих обоих событиях; + +[ ] Добавить проверку во время инициализации сервера наличия PID-файлов + демонов-воркеров: + - Если такие есть -- проверить, что это за процессы; + - Если такие процессы не существуют или не являются воркерами -- удалить + эти PID-файлы; + - Если процессы существуют, являются воркерами и есть информация о них + в БД -- проверяем соответствие PID и WID. + -- Если все совпадает -- удаляем PID-файлы и далее создаем объекты + воркеров по информации в БД; + -- [Q#3] Если есть несовпадения -- убиваем воркер, PID-файл и + записи в БД; + - [Q#2] Если процессы существуют, являются воркерами, но в БД о них нет + информации -- наверное, удаляем PID-файлы и пытаемся убить этих + воркеров с помощью их PID. + + +Интерфейс сервера +----------------- +REST-интерфейс (вроде). Представляем состояния воркеров в виде ресурсов или +объектов. В соответствии с HATEAOS присылаем URI на ресурсы и действия воркера, +которые доступны в тот или иной момент. Для этого применяем формат +json-сообщений напоминающий HAL или CPHL (не очень распространенный формат). +Различие от HAL в наличии атрибута "methods" в "_links". + +* root -- корневой ресурс, являющийся точкой входа для всего API. Возможно, + стоит создать алиас /index или заменить на него. + + - GET / -- получить доступные корневые ресурсы; + * request: + + null + + * response: + Media-Type: application/hal+json + Cache-Control: public + + { + "data": { + // Some information about server. + }, + "_links": { + "commands": { + "href": "/commands/" + }, + "workers": { + "href": "/workers/" + } + } + } + + { + "meta": { + // Server info + }, + "links": { + } + } + +* сommands -- совокупность данных о командах, доступных для запуска на + сервере. + + - GET /commands/ -- получить список команд; + * request: + Media-Type: application/json + + { + "gui": {true/false} + } + + * response: + Media-Type: application/hal+json + Cache-Control: private + + { + "{command_id}": { + "data": { + // Some command info. + }, + "_links": { + "self": { + "href": "/commands/{CID}" + }, + "parameters": { + "href": "/commands/{CID}/parameters" + }, + "cofigure": { + "href": "/configs/{CID}" + } + } + }, + ... + } + + - GET /commands/{command}/ -- получить информацию об указанной команде, a + также ее параметры. Запрос используемый + консольным клиентом; + * request: + + null + + * response: + Media-Type: application/hal+json + Cache-Control: private + + { + "data": { + // Some command info. + }, + "_links": { + "self": { + "href": "/commands/{CID}" + }, + "parameters": { + "href": "/commands/{CID}/parameters" + } + "cofigure": { + "href": "/configuration/{CID}" + } + }, + "_embedded": { + "parameters": { + "data": [ + { + "group_id": "{group_id}", + "parameters": [...] + }, + ... + ], + "_links": + "self": { + "href": "/commands/{CID}/parameters" + } + } + } + } + + - GET /commands/{CID}/parameters -- получить параметры указанной команды; + * request: + + null + + * response: + + { + "data": [ + { + "group_id": "{group_id}", + "parameters": [...] + }, + ... + ], + "_links": { + "self": { + "href": "/commands/{CID}/parameters" + } + } + } + +* configs -- совокупность объектов воркеров, находящихся в состоянии + конфигурации. + + - POST /configs/{command} -- создать конфигурацию; + 201 -- конфигурация уже создана; + 404 -- команды с указанным id нет; + 400 -- неправильный формат запроса. + * request: + + null + + * response: + + { + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "execute": { + "href": "/executions/{WID}" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + - PATCH /configs/{WID}/parameters -- изменить конфигурацию + указанными в теле значениями; + 404 -- конфигурации с указанным + WID нет; + 400 -- неправильный формат + запроса. + * request: + + [ + {"id": "{param_id}", "value": "{param_value}"}, + ... + ] + + * response: + + { + "data": { + "{param_id}": "{error_message}", + ... + }, + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + OR + + { + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "execute": { + "href": "/executions/{WID}" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + - PUT /configs/{WID}/parameters -- заменить конфигурацию с + использованием указанных в теле + запроса значений. Пустой набор + значений позволяет сбросить + конфигурацию; + * request: + + [ + {"id": "{param_id}", "value": "{param_value}"}, + ... + ] + + * response: + + { + "data": { + "{param_id}": "{error_message}", + ... + }, + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + OR + + { + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "execute": { + "href": "/executions/{WID}" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + - DELETE /configs/{WID} -- закончить конфигурации без выполнения + команды. + * request: + + null + + * response: + + [ + { + "data": { + "type": "output", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}", + } + }, + ... + ] + +* executions -- совокупность объектов воркеров, находящихся в состоянии + исполнения. + + - POST /executions/{WID} -- создать новое исполнение из конфигурации; + 201 -- исполнение создано; + 409 -- исполнение с этим WID уже есть; + 400 -- неправильный формат запроса. + 404 -- конфигурация не найдена. + * request: + + null + + * response: + + - Response Code: 201 + + { + "_links": { + "output": { + "href": "/executions/{WID}/output" + }, + "stop": { + "href": "/executions/{WID}" + } + } + } + + - DELETE /executions/{WID} -- удалить исполнение, происходит путем + отправки в скрипт SIGINT; + * request: + + null + + * response (возвращает список непереданных сообщений, если они есть): + + [ + { + "data": { + "type": "output", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}", + } + }, + ... + ] + + - GET /executions/{WID}/output -- получить список имеющихся сообщений от + команды; + * request: + + null + + * response (необходимость остановить выполнение определяется по + Response Code): + + [ + { + "data": { + "type": "output", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}", + } + }, + { + "data": { + "type": "input", + "text": "{input_message}" + "source": "{source_script}" + }, + "_links": { + "input": { + "href": "/executions/{WID}/input", + } + } + } + ] + + - PATCH /executions/{WID}/input -- отправить в воркер некоторый ввод; + * request (если null, значит ввод прерывается): + + { + "data": {input_data/null} + } + + * response: + - Response Code: 200 + + null + + - Response Code: 404 + + { + "error": "Execution {WID} is not found" + } + + - Response Code: 400 + + { + "error": "Input message is not correct" + } + +* workers -- вся совокупность воркеров. + + - GET /workers/ -- получить список демонов-воркеров и информацию о них; + * request: + + null + + * response: + + { + "{WID}": { + "data": { + "socket": "{socket_path}", + "command": "{worker_command}", + "pid": "{daemon_pid}", + } + "_links": { + "kill": { + "href": "/workers/{WID}", + "methods": ["delete"] + } + } + }, + ... + } + + - DELETE /workers/{WID} -- отправить сигнал SIGKILL демону-воркеру; + * request: + + null + + * response (сообщения полученные от убитого воркера): + + [ + { + "data": { + "type": "output", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}", + } + }, + ... + ] + + +Протокол взаимодействия воркера и сервера +----------------------------------------- +Все сообщения представляют собой json-структуры, включающие в себя 3 поля: + - state -- состояние воркера: + * Если сообщение пришло воркеру от сервера, тогда это поле указывает + воркеру, какое состояние он должен принять; + * Если сообщение пришло серверу от воркера, тогда это поле указывает на + текущее состояние воркера. + + - status -- статус текущего состояния: + 0 -- состояние корректно; + 1 -- состояние некорректно. + + - data -- некоторые данные, структура которых может быть любой. + +1. Данные конфигурации от клиента. + a. Продолжить конфигурацию указанными значениями: + + { + "state": "config", + "status": 0, + "data": [ + {"id": "{param_id}", "value": "{parameter_value}"}, + ... + ] + } + + b. Сбросить конфигурацию и продолжить указанными значениями: + + { + "state": "config", + "status": 1, + "data": [ + {"id": "{param_id}", "value": "{parameter_value}"}, + ... + ] + } + +2. Ответ воркера по данным конфигурации. + a. Все параметры конфигурации корректны: + + { + "state": "config", + "status": 0, + "data": null + } + + b. Некоторые параметры конфигурации некорректны: + + { + "state": "config", + "status": 1, + "data": { + "{param_id}": "{error_message}", + ... + } + } + +3. Запрос на окончание конфигурации от клиента. + a. Закончить конфигурацию, начать выполнение команды: + + { + "state": "finish", + "status": 0, + "data": null + } + + b. Закончить конфигурацию и работу воркера: + + { + "state": "finish", + "status": 1, + "data": null + } + +4. Ответ воркера на запрос об окончании конфигурации: + a. Закончить конфигурацию, начать выполнение команды: + + { + "state": "finish", + "status": 0, + "data": null + } + + b. Закончить конфигурацию и работу воркера: + + { + "state": "finish", + "status": 1, + "data": { + "{param_id}": "{error_message}", + ... + } + } + +5. Сообщение с выводом воркера. + a. Сообщение из скрипта при его "штатной" работe: + + { + "state": "exec", + "status": 0, + "data": { + "type": "message", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}" + } + } + + b. Сообщение об ошибке приводящей к завершению выполнения скрипта и + команды: + + { + "state": "exec", + "status": 1, + "data": { + "error": "{error_message}", + "source": "{source_script}" + } + } + +6. Сообщение с запросом на ввод. + a. Состояние "input" -- воркер в состоянии ожидания ввода: + { + "state": "input", + "status": 0, + "data": { + "text": "{message_text}", + "source": "{source_script}" + } + } + +7. Сообщение от клиента с данными. + a. Ввод успешно выполнен и передается: + + { + "state": "input", + "status": 0, + "data": "{client_data}" + } + + b. Ввод данных прерван, данных не будет: + + { + "state": "input", + "status": 1, + "data": null + } + +8. Сообщение от воркера о получении ввода. + a. Ввод успешно получен, о чем говорит переход в состояние выполнения со + статусом 0, т.е. без ошибок: + + { + "state": "exec", + "status": 0, + "data": null + } + +9. Сообщение воркера об окончании работы: + a. Успешное окончание работы: + + { + "state": "finish", + "status": 0, + "data": null + } + + b. Работа воркера была прервана или закончилась с ошибками: + + { + "state": "finish", + "status": 1, + "data": [ + "{error_message}", + ... + ] + } diff --git a/calculate/server/ipc_draft/async_client.py b/calculate/server/ipc_draft/async_client.py new file mode 100755 index 0000000..01af5e3 --- /dev/null +++ b/calculate/server/ipc_draft/async_client.py @@ -0,0 +1,165 @@ +#! env/bin/python3 +import sys +import asyncio +import aiohttp +import logging + +from typing import List, Tuple, Any + + +loop = asyncio.get_event_loop() + + +SERVER_DOMAIN = "http://127.0.0.1:2007" + + +LOG_LEVELS = {logging.ERROR: "ERROR", + logging.INFO: "INFO", + logging.WARNING: "WARNING", + logging.DEBUG: "DEBUG", + logging.CRITICAL: "CRITICAL", + } + + +async def check_server(client: aiohttp.ClientSession): + try: + async with client.get(f"{SERVER_DOMAIN}/") as response: + assert response.status == 200 + + data = await response.json() + assert data == {"status": "active"} + + print(f"Connected to the Calculate Server on {SERVER_DOMAIN}") + return True + except Exception: + print("Can not connect to the calculate server.") + return False + + +async def get_commands(client: aiohttp.ClientSession): + async with client.get(f"{SERVER_DOMAIN}/commands") as response: + assert response.status == 200 + data = await response.json() + return data + + +async def create_worker(client: aiohttp.ClientSession, command: str, + arguments: List[str]): + worker_args = {"arguments": arguments} + async with client.post(f"{SERVER_DOMAIN}/workers/{command}", + json=worker_args) as response: + assert response.status == 200 + data = await response.json() + return data["wid"] + + +async def start_worker(client: aiohttp.ClientSession, wid: int): + async with client.post(f"{SERVER_DOMAIN}/workers/{wid}/start") as response: + assert response.status == 200 + data = await response.json() + return data + + +async def get_worker_messages(client: aiohttp.ClientSession, wid: int): + async with client.get(f"{SERVER_DOMAIN}/workers/{wid}/messages" + ) as response: + assert response.status == 200 + data = await response.json() + return data["data"] + + +async def send_data_to_worker(client: aiohttp.ClientSession, wid: int, + data: Any): + first_try = True + while True: + answer = {"data": data} + try: + async with client.post(f"{SERVER_DOMAIN}/workers/{wid}/send", + json=answer) as response: + assert response.status == 200 + data = await response.json() + return + except aiohttp.client_exceptions.ClientOSError: + if first_try: + first_try = False + continue + else: + raise + # return data + + +async def finish_worker(client: aiohttp.ClientSession, wid: int): + async with client.post(f"{SERVER_DOMAIN}/workers/{wid}/finish" + ) as response: + assert response.status == 200 + data = await response.json() + return data + + +async def main(): + print("Calculate Console Client 0.0.1") + async with aiohttp.ClientSession( + connector=aiohttp.TCPConnector(force_close=False)) as client: + if not await check_server(client): + return + + commands = await get_commands(client) + command, command_args = get_console_command() + message = check_command(command, commands) + if message: + print(message) + return + + wid = await create_worker(client, command, command_args) + try: + print("CREATED WORKER ID:", wid) + status_data = await start_worker(client, wid) + if status_data.get("status", None) == "error": + print("ERROR:", status_data.get("description", "NONE")) + return + + finished = False + while not finished: + messages = await get_worker_messages(client, wid) + for message in messages: + if message["type"] == "output": + print(f"{LOG_LEVELS[message['level']]}:" + f" {message['msg']}") + elif message["type"] == "input": + print(message["msg"]) + input_data = input(">> ") + await send_data_to_worker(client, wid, input_data) + elif (message["type"] == "control" + and message["action"] == "finish"): + finished = True + finally: + await finish_worker(client, wid) + + +def get_console_command() -> Tuple[str, List[str]]: + offset = 1 if sys.argv[0].endswith("client.py") else 0 + + if len(sys.argv) < 1 + offset: + command = None + command_args = [] + else: + command = sys.argv[offset] + command_args = sys.argv[1 + offset:] + + return command, command_args + + +def check_command(command: str, available_commands: List[str]) -> str: + if command is None: + return "Command is not set." + if command not in available_commands: + return "Command is not available." + return '' + + +if __name__ == "__main__": + try: + loop.run_until_complete(main()) + except KeyboardInterrupt: + print("\r<< Keyboard interrupt.") + loop.close() diff --git a/calculate/server/ipc_draft/cl-command-0 b/calculate/server/ipc_draft/cl-command-0 new file mode 120000 index 0000000..59b9c19 --- /dev/null +++ b/calculate/server/ipc_draft/cl-command-0 @@ -0,0 +1 @@ +client.py \ No newline at end of file diff --git a/calculate/server/ipc_draft/cl-command-1 b/calculate/server/ipc_draft/cl-command-1 new file mode 120000 index 0000000..59b9c19 --- /dev/null +++ b/calculate/server/ipc_draft/cl-command-1 @@ -0,0 +1 @@ +client.py \ No newline at end of file diff --git a/calculate/server/ipc_draft/cl-command-2 b/calculate/server/ipc_draft/cl-command-2 new file mode 120000 index 0000000..59b9c19 --- /dev/null +++ b/calculate/server/ipc_draft/cl-command-2 @@ -0,0 +1 @@ +client.py \ No newline at end of file diff --git a/calculate/server/ipc_draft/cl-command-3 b/calculate/server/ipc_draft/cl-command-3 new file mode 120000 index 0000000..59b9c19 --- /dev/null +++ b/calculate/server/ipc_draft/cl-command-3 @@ -0,0 +1 @@ +client.py \ No newline at end of file diff --git a/calculate/server/ipc_draft/client.py b/calculate/server/ipc_draft/client.py new file mode 100755 index 0000000..11ba7a5 --- /dev/null +++ b/calculate/server/ipc_draft/client.py @@ -0,0 +1,183 @@ +#! env/bin/python3 +import sys +import requests +import logging + +from typing import Tuple, List, Union, Dict + +from pprint import pprint + + +LOG_LEVELS = {logging.ERROR: "ERROR", + logging.INFO: "INFO", + logging.WARNING: "WARNING", + logging.DEBUG: "DEBUG", + logging.CRITICAL: "CRITICAL", + } + + +def main(): + # Обращаемся к корню сервера для получения списка доступных ресурсов. + data, links = get_root_data("http://127.0.0.1:2007") + if data is None: + return + print(f"Connected to {data['name']}") + + print("\nRoot links:") + pprint(links) + + # Получаем данные о текущей команде. + cl_command, cl_parameters = get_console_command() + print_console_parameters(cl_parameters) + data, links, parameters = find_command(cl_command, links) + if data is None: + return + print_command_info(data) + print_parameters(parameters) + + links = start_command_configuration(links) + + print("\nConfig links:") + pprint(links) + + print("\nSetting Parameters") + print("\nLinks:") + data, links = set_parameters(links, cl_parameters) + pprint(links) + + if data is not None: + print_config_errors(data) + + print("\nCancel configuration:") + result = cancel_configuration(links) + pprint(result) + + +def get_root_data(base_url: str) -> Tuple[Union[dict, None], + Union[dict, None]]: + try: + response = requests.get(f"{base_url}/") + if response.status_code == 200: + data = response.json()["data"] + links = response.json()["_links"] + + return data, links + else: + detail = response.json()['detail'] + print(f"{response.status_code}: {detail}") + except Exception as error: + print("ERROR:", str(error)) + return None, None + + +def find_command(console_command: str, links: dict) -> dict: + find_uri = links["find_command"]["href"].format( + console_command=console_command, + is_gui=0) + response = requests.get(find_uri) + + if response.status_code == 200: + json = response.json() + data = json["data"] + links = json["_links"] + parameters = json["_embedded"]["parameters"] + return data, links, parameters + else: + detail = response.json()['detail'] + print(f"{response.status_code}: {detail}") + return None, None, None + + +def get_parameters(links: dict) -> List[dict]: + params_uri = links["parameters"]["href"] + response = requests.get(params_uri) + if response.status_code == 200: + return response.json() + else: + print(f"Response Code: {response.status_code}, {response.text}") + return None + + +def start_command_configuration(links: dict) -> Union[dict, None]: + configure_uri = links["configure"]["href"] + response = requests.post(configure_uri) + if response.status_code == 200: + json = response.json() + return json["_links"] + else: + print(f"Response Code: {response.status_code}, {response.text}") + return None + + +def set_parameters(links: dict, parameters: List[dict]) -> Dict[str, dict]: + configure_uri = links["configure"]["href"] + response = requests.patch(configure_uri, json=parameters) + if response.status_code == 200: + json = response.json() + data = json.get("data", None) + links = json["_links"] + return data, links + else: + print(f"Response Code: {response.status_code}, {response.text}") + return None, None + + +def cancel_configuration(links: dict) -> List[dict]: + configure_uri = links["cancel"]["href"] + response = requests.delete(configure_uri) + if response.status_code == 200: + return response.json() + else: + print(f"Response Code: {response.status_code}, {response.text}") + return None + + +def get_console_command() -> Tuple[str, List[str]]: + offset = 1 if sys.argv[0].endswith("client.py") else 0 + command_args = [] + + if len(sys.argv) < 1 + offset: + command = None + else: + command = sys.argv[offset].split("/")[-1] + args = sys.argv[1 + offset:] + if len(args) >= 2 and not (len(args) % 2): + for index in range(0, len(args) - len(args) % 2, 2): + id, value = args[index: index + 2] + command_args.append({"id": id.strip("-"), + "value": value}) + + return command, command_args + + +def print_console_parameters(parameters: dict): + print("\nConsole parameters:") + for parameter in parameters: + print(f" {parameter['id']} = {parameter['value']}") + + +def print_command_info(data: dict) -> None: + print(f"\nCurrent command: {data['id']}\n" + f" category: {data['category']}\n" + f" title: {data['title']}") + + +def print_parameters(parameters_data: list) -> None: + parameters = parameters_data["data"] + print("\nParameters:") + for group in parameters: + print(f" {group['group_id']}") + group_parameters = group["parameters"] + for parameter in group_parameters: + print(f" parameter:\t{parameter['id']}\n" + f" default:\t{parameter['default']}\n") + + +def print_config_errors(errors: Dict[str, str]) -> None: + print("\nConfiguration errors:") + for parameter_id, error in errors.items(): + print(f" {parameter_id}: {error}") + + +if __name__ == "__main__": + main() diff --git a/calculate/server/ipc_draft/commands.py b/calculate/server/ipc_draft/commands.py new file mode 100644 index 0000000..2794984 --- /dev/null +++ b/calculate/server/ipc_draft/commands.py @@ -0,0 +1,77 @@ +from typing import List, Callable, Dict +from scripts import script_0, script_1, script_2 +from collections import OrderedDict + + +class Command: + '''Тестовый класс команд''' + def __init__(self, + command_id: str, + command: str, + category: str, + title: str, + scripts: List[Callable], + parameters: Dict[str, dict]): + self.id = command_id + self.command = command + self.category = category + self.command = command + self.title = title + self.scripts = scripts + self.parameters = parameters + + +command_0 = Command(command_id="command_0", + category="Test", + title="Test command number 0", + command="cl-command-0", + scripts=OrderedDict({"script_0": script_0, + "script_1": script_1}), + parameters=[{"group_id": "group_0", + "parameters": [{"id": "value_0", + "default": 253}, + {"id": "value_1", + "default": 413}] + } + ] + ) + + +command_1 = Command(command_id="command_1", + category="Test", + title="Test command number 1", + command="cl-command-1", + scripts=OrderedDict({"script_0": script_0, + "script_1": script_1, + "script_2": script_2}), + parameters=[{"group_id": "group_1", + "parameters": [{"id": "value_0", + "default": 35}, + {"id": "value_1", + "default": 41}] + }, + {"group_id": "group_2", + "parameters": [{"id": "value_2", + "default": 11}] + } + ] + ) + + +command_2 = Command(command_id="command_2", + category="Test", + title="Test command number 2", + command="cl-command-2", + scripts=OrderedDict({"script_0": script_0, + "script_2": script_2}), + parameters=[{"group_id": "group_0", + "parameters": [{"id": "value_0", + "default": 253}] + }, + {"group_id": "group_1", + "parameters": [{"id": "value_2", + "default": 413}, + {"id": "value_5", + "default": 23}] + }] + ) diff --git a/calculate/server/ipc_draft/daemon.py b/calculate/server/ipc_draft/daemon.py new file mode 100644 index 0000000..3b30ec7 --- /dev/null +++ b/calculate/server/ipc_draft/daemon.py @@ -0,0 +1,121 @@ +# import time +from typing import Union, List, Optional, Dict +from multiprocessing.connection import Listener + +from commands import Command +from io_module import IOModule + + +def daemon(listener: Listener, wid: int, base_dir: str, command: Command): + """Daemon main function.""" + with IOModule(listener, command.id) as io: + io.script = "worker" + try: + # int("lolkek") + parameters = configure(io, + get_default_parameters(command.parameters)) + except KeyboardInterrupt: + # Остановлено пользователем. + send_finish(io) + return + except Exception as error: + # Остановлено из-за ошибки. + send_finish(io, errors=[str(error)]) + return + + try: + for name, script in command.scripts.items(): + script_func = script[0] + + parameters = configure(io, dict()) + + io.set_info(f"Running script '{name}'") + io.script = name + + script_func(io, parameters) + + io.script = "worker" + + for num in range(5): + io.set_error(f"test_error_{num}") + raise Exception("critical error LOL") + + io.set_error("there is no life after CRITICAL error") + + except Exception as error: + io.set_error(str(error)) + finally: + io.set_info("Command is done.") + io.send({"type": "finish", "msg": "finishing."}) + for num in range(5): + io.set_info(f"After finish message_{num}.") + + +def configure(io: IOModule, parameters: dict) -> Union[None, dict]: + # Копируем чтобы уже для тестов реализовать возможность сброса. + processing_parameters = parameters.copy() + + while True: + errors = [] + server_message = io.receive() + + state = server_message["state"] + status = server_message["status"] + + if state == "config": + if status == 1: + # Сброс параметров. + processing_parameters = parameters.copy() + values: list = server_message.get("data", None) + if values is not None: + # Модифицируем параметры. + errors = modify_parameters(processing_parameters, values) + elif state == "finish": + if status == 0: + # Заканчиваем конфигурацию и возвращаем полученные параметры + # если статус 1. + return parameters + # Останавливаем конфигурацию и работу воркера если статус 1. + raise KeyboardInterrupt() + + if errors: + send_config_status(io, errors=errors) + else: + send_config_status(io) + + +def modify_parameters(current: dict, new: list) -> None: + errors = {} + for parameter in new: + parameter_id = parameter["id"] + parameter_value = parameter["value"] + if parameter_id in current: + try: + parameter_value = int(parameter_value) + current[parameter_id] = parameter_value + except ValueError as error: + errors[parameter_id] = str(error) + else: + errors[parameter_id] = f'Parameter "{parameter_id}" is not found.' + return errors + + +def send_finish(io: IOModule, errors: Optional[List[str]] = []) -> None: + io.send({"state": "finish", + "status": int(bool(errors)), + "data": errors}) + + +def send_config_status(io: IOModule, + errors: Optional[Dict[str, str]] = {}) -> None: + io.send({"state": "config", + "status": int(bool(errors)), + "data": errors}) + + +def get_default_parameters(parameters_description): + parameters = {} + for group in parameters_description: + for parameter in group["parameters"]: + parameters[parameter["id"]] = parameter["default"] + return parameters diff --git a/calculate/server/ipc_draft/example/ipc.py b/calculate/server/ipc_draft/example/ipc.py new file mode 100644 index 0000000..b3b092d --- /dev/null +++ b/calculate/server/ipc_draft/example/ipc.py @@ -0,0 +1,143 @@ +# Copyright 2017 Dan Krause +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import socketserver +import socket +import struct +import json + + +class IPCError(Exception): + pass + + +class UnknownMessageClass(IPCError): + pass + + +class InvalidSerialization(IPCError): + pass + + +class ConnectionClosed(IPCError): + pass + + +def _read_objects(sock): + header = sock.recv(4) + if len(header) == 0: + raise ConnectionClosed() + size = struct.unpack('!i', header)[0] + data = sock.recv(size - 4) + if len(data) == 0: + raise ConnectionClosed() + return Message.deserialize(json.loads(data)) + + +def _write_objects(sock, objects): + data = json.dumps([o.serialize() for o in objects]) + sock.sendall(struct.pack('!i', len(data) + 4)) + sock.sendall(data) + + +def _recursive_subclasses(cls): + classmap = {} + for subcls in cls.__subclasses__(): + classmap[subcls.__name__] = subcls + classmap.update(_recursive_subclasses(subcls)) + return classmap + + +class Message(object): + @classmethod + def deserialize(cls, objects): + classmap = _recursive_subclasses(cls) + serialized = [] + for obj in objects: + if isinstance(obj, Message): + serialized.append(obj) + else: + try: + serialized.append(classmap[obj['class']](*obj['args'], + **obj['kwargs'])) + except KeyError as e: + raise UnknownMessageClass(e) + except TypeError as e: + raise InvalidSerialization(e) + return serialized + + def serialize(self): + args, kwargs = self._get_args() + return {'class': type(self).__name__, 'args': args, 'kwargs': kwargs} + + def _get_args(self): + return [], {} + + def __repr__(self): + r = self.serialize() + args = ', '.join([repr(arg) for arg in r['args']]) + kwargs = ''.join([', {}={}'.format(k, repr(v)) + for k, v in r['kwargs'].items()]) + name = r['class'] + return '{}({}{})'.format(name, args, kwargs) + + +class Client(object): + def __init__(self, server_address): + self.addr = server_address + if isinstance(self.addr, str): + address_family = socket.AF_UNIX + else: + address_family = socket.AF_INET + self.sock = socket.socket(address_family, socket.SOCK_STREAM) + + def connect(self): + self.sock.connect(self.addr) + + def close(self): + self.sock.close() + + def __enter__(self): + self.connect() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def send(self, objects): + _write_objects(self.sock, objects) + return _read_objects(self.sock) + + +class Server(socketserver.ThreadingUnixStreamServer): + def __init__(self, server_address, callback, bind_and_activate=True): + if not callable(callback): + callback = lambda x: [] + + class IPCHandler(socketserver.BaseRequestHandler): + def handle(self): + while True: + try: + results = _read_objects(self.request) + except ConnectionClosed: + return + _write_objects(self.request, callback(results)) + + if isinstance(server_address, str): + self.address_family = socket.AF_UNIX + else: + self.address_family = socket.AF_INET + + socketserver.TCPServer.__init__(self, server_address, IPCHandler, + bind_and_activate) diff --git a/calculate/server/ipc_draft/interfaces.txt b/calculate/server/ipc_draft/interfaces.txt new file mode 100644 index 0000000..571dd30 --- /dev/null +++ b/calculate/server/ipc_draft/interfaces.txt @@ -0,0 +1,830 @@ +Вопросы +------- +1. >> Что является ресурсом? + * commands -- информация о командах, их список, группы, описания и тд. + * workers(?) -- воркеры, исполняющие команды. Сомнительно. + Возможно есть смысл скрыть реализацию, т.е. воркеров, под некоторыми + другими сущностями. Например, execution и configuration. + workers -- мб оставить как дополнительный низкоуровневый интерфейс. + << Ресурсы: commands, configuration, execution, workers (low_level) + +2. >> Что делать с демонами-воркерами, обнаруженными при инциализации сервера, + но не зарегистрированными в БД? + << Убиваем их и удаляем PID-файлы. + +3. >> Что делать с демонами-воркерами, для которых найдены PID-файлы, по PID + есть информация в БД, но WID из имени PID-файла не совпадает с WID в БД. + << Убиваем воркера, удаляем PID-файл и запись в БД. + +4. >> Стоит ли убрать разделение команд и их id? + << Не стоит. + +5. >> Нужна ли возможность сброса текущих параметров конфигурации? + << Да. + + +Взаимодействие с воркером с точки зрения сервера +------------------------------------------------ +1. Сервер получает от клиента запрос, POST /configuration/{command_id}. Далее: + + * Сервер создает объект воркера команды и запускает с его помощью + демона-рабочего, оставляя его в режиме ожидания конфигурационных данных. + + * После создания объекта воркера в БД сохраняется основная информация + необходимая для воссоздания объекта воркера в новом процессе сервера. Такая + ситуация может возникнуть при переходе на новую версию сервера: + + - int wid -- идентификатор воркера; + + - str socket -- путь к сокету для чтения данных воркера; + + - int daemon_pid -- PID демона-воркера для взаимодействия с ним. + + * В ответ на запрос сервер получает список доступных ресурсов, включающих + в себя в том числе действия воркера, а также URI этих ресурсов и + доступные для этого URI http-методы. На данном этапе: + + - configure (patch) -- изменить некоторые параметры конфигурации; + + - cancel (delete) -- закончить конфигурацию без продолжения выполнения. + По сути удаляет данную конфигурацию как ресурс; + + - execute (post) -- запустить выполнение команды с дефолтной + конфигурацией. По сути удаляет конфигурацию как ресурс и создает + новый ресурс -- выполнение. + +2. Сервер получает от клиента запрос PATCH /configuration/{WID}/parameters, + содержащий в теле конфигурационные данные. Далее: + + * Формируется конфигурационное сообщение (1a), содержащее в себе список + объектов, описывающих один или несколько параметров команды, которое + затем отправляется воркеру. + + * Oжидается ответ от воркера (1a) с информацией о результатах обработки + параметров, найденных ошибках и т.д, который пересылается клиенту. + + * Конфигурационный цикл повторяется. + + Возможно стоит добавить на стороне сервера таймаут при получении ответа от + воркера. + +3. Сервер получает от клиента запрос POST /executions/{WID}, указывающий + закончить конфигурацию команды и начать выполнение. + Далее: + + * Формируется сообщение воркеру (3a), сообщающее уже ему об окончании цикла + конфигурации. + + * Если есть неисправленные ошибки, воркер отвечает на него сообщением (4b), + содержащим оповещение о том, что он заканчивает работу; + Если ошибок нет, то сообщением (4a), оповещающим об успешном запуске + скрипта команды. + + * В случае, когда обработка параметров прошла с ошибками -- сразу после + выдачи сообщения об ошибках конфигурации, воркер отправляет сообщение + об окончании своей работы, содержащее список ошибок (9b) и (?) ждет + подтверждение от сервера (?). + +4. Сервер получает от клиента запрос GET /executions/{WID}/output на + получение имеющихся выходных данных воркера. Далее: + + * Cервер читает все, что есть на сокете воркера. Сообщения (5a, 5b или 6a) + на сокете разделены нулевым символом '\0'. Собирается список полных + сообщений на сокете, формируется сообщение, содержащее этот список, + которое отправляется клиенту. + +5. Сервер получает от клиента запрос PATCH /executions/{WID}/input на передачу + воркеру ввода, который был запрошен последним в одном из сообщений ранее + переданных клиенту через сервер. Далее: + + * Сервер формирует сообщение (7a) воркеру с данными ввода и отправляет его + воркеру. Если данных нет, что означает, что ввод был сброшен + пользователем, воркер отправляет серверу сообщение (7b) об некорректном + вводе, а метод input объекта ввода/вывода кидает в скрипте специальное + исключение. + + Возможно, необходимо убеждаться в том, что данные успешно получены воркером. + Воркер должен ответить сообщением (8a) об успешном получении данных. + + - Если данное сообщение не получено в течение некоторого времени + (таймаута) -- выполняем некоторые действия по диагностике и устранению + последствий ошибки, затем отправляем клиенту сообщение, сообщающее о том, + что воркер закончил работу, и содержащее последние сообщения, полученные + от воркера, если таковые имеются. + + - Если сообщение об успешном вводе в воркер получено -- отправляем + соответствующее сообщение клиенту. + +6. Работа воркера закончилась в результате успешного выполнения команды, + или было прервано клиентом командой stop, или по причине ошибок, возникших + при выполнении скриптов команды. Далее: + + * Воркер отправляет на сервер одно из двух специальных сообщений, + оповещающих об окончании работы: + + - Содержащее также информацию об ошибках (9b), если таковые + были, неотправленных по каким-то причинам сообщениях (?) или о факте + выполнения команды stop. + + - Содержащие только объявление об окончании выполнения команды, то есть + состояния run. + + В зависимости от того, возможно ли чтение из сокета, когда процесс воркера + уже закрыт сервер будет или не будет отправлять сообщение разрешающее + воркеру прекратить работу, но клиент его видеть не должен. По факту + получения конечного сообщения и, возможно, данных вместе с ним, клиенту + будет присылаться специальное сообщение. + + +Контекст воркера +---------------- +worker_state -- состояние воркера; +socket_context -- контекст чтения сокета воркера; +worker_output -- вывод воркера перед его выключением сервера. + + +Что делать +---------- +[*] Проверить возможность чтения сервером данных из сокета уже закрытого + воркера. Если такой возможности нет, предусмотреть обработку завершения и + успешной, и некорректной работы воркера с передачей всех необходимых + сообщений. Сделать это внутри воркера. + Итог: чтение из сокета убитого демона возможно. + +[ ] Добавить в воркер цикл конфигурации или его тестовый вариант; + +[*] Убрать на стороне интерфейса клиента лишний шаг создания объекта воркера; + +[*] Убрать self._runner_process и self._pid_file, они в таком случае не нужны; + +[*] PID-файл демона удаляем сразу после чтения; + +[ ] Реализовать описанный ниже протокол взаимодействия сервера с воркером; + +[ ] Добавить в метод kill воркера чтение всего содержимого сокета перед его + удалением. + +[ ] Разработать HAL-формат сообщений об ошибках вместо HTTPException. + +[*] Сделать метод read воркера асинхронным. + +[ ] На случай если воркер убит через SIGKILL -- поскольку обработку + такого завершения работы воркера в самом воркере организовать невозможно, + добавить дополнительную проверку is_alive воркера и предусмотреть обработку + этой ситуации. + +[ ] Добавить главный блок try-catch-finally в воркер. В раздел finally добавить + отправку сообщения об окончании работы воркера. В зависимости от + возможности читать данный из сокета законченного процесса воркера, ждать + или не ждать ответ сервера по поводу окончания работы воркера; + +[ ] Добавить предварительно деление воркеров по состояниям, вероятно добавить + WorkersPool; + +[ ] Добавить состояния воркера: + - config -- воркер в состоянии конфигурации, Т.е. ждёт значения + параметров или собщение об окончании конфигурации; + - exec -- воркер в процессе выполнения команды; + - input -- воркер в процессе ожидания ввода пользователя; + - finish -- воркер в состоянии завершения некоторого состояния. + +[ ] Реализовать предварительно REST API в соответствии с наработками; + +[ ] Решить, какое значение должны иметь заголовки Content-Type и Accept: + - application/json; + - application/hal+json; + - application/vnd.cphl+json; + - application/vnd.api+json. + Подумать над версионированием API; + +[ ] Добавить в ответы сервера обозначения о кэшируемости и некэшируемости, + проработать кэширование в целом, определить, что должно кэшироваться, а что + нет; + +[ ] Проработать механизм остановки взаимодействия сервера с клиентом и + возвращения к нему после переподключения клиента, перезагрузки сервера, а + также при этих обоих событиях; + +[ ] Добавить проверку во время инициализации сервера наличия PID-файлов + демонов-воркеров: + - Если такие есть -- проверить, что это за процессы; + - Если такие процессы не существуют или не являются воркерами -- удалить + эти PID-файлы; + - Если процессы существуют, являются воркерами и есть информация о них + в БД -- проверяем соответствие PID и WID. + -- Если все совпадает -- удаляем PID-файлы и далее создаем объекты + воркеров по информации в БД; + -- [Q#3] Если есть несовпадения -- убиваем воркер, PID-файл и + записи в БД; + - [Q#2] Если процессы существуют, являются воркерами, но в БД о них нет + информации -- наверное, удаляем PID-файлы и пытаемся убить этих + воркеров с помощью их PID. + + +Интерфейс сервера +----------------- +REST-интерфейс (вроде). Представляем состояния воркеров в виде ресурсов или +объектов. В соответствии с HATEAOS присылаем URI на ресурсы и действия воркера, +которые доступны в тот или иной момент. Для этого применяем формат +json-сообщений напоминающий HAL или CPHL (не очень распространенный формат). +Различие от HAL в наличии атрибута "methods" в "_links". + +* root -- корневой ресурс, являющийся точкой входа для всего API. Возможно, + стоит создать алиас /index или заменить на него. + + - GET / -- получить доступные корневые ресурсы; + * request: + + null + + * response: + Media-Type: application/hal+json + Cache-Control: public + + { + "data": { + // Some information about server. + }, + "_links": { + "commands": { + "href": "/commands/" + }, + "workers": { + "href": "/workers/" + } + } + } + + { + "meta": { + // Server info + }, + "links": { + } + } + +* сommands -- совокупность данных о командах, доступных для запуска на + сервере. + + - GET /commands/ -- получить список команд; + * request: + Media-Type: application/json + + { + "gui": {true/false} + } + + * response: + Media-Type: application/hal+json + Cache-Control: private + + { + "{command_id}": { + "data": { + // Some command info. + }, + "_links": { + "self": { + "href": "/commands/{CID}" + }, + "parameters": { + "href": "/commands/{CID}/parameters" + }, + "cofigure": { + "href": "/configs/{CID}" + } + } + }, + ... + } + + - GET /commands/{command}/ -- получить информацию об указанной команде, a + также ее параметры. Запрос используемый + консольным клиентом; + * request: + + null + + * response: + Media-Type: application/hal+json + Cache-Control: private + + { + "data": { + // Some command info. + }, + "_links": { + "self": { + "href": "/commands/{CID}" + }, + "parameters": { + "href": "/commands/{CID}/parameters" + } + "cofigure": { + "href": "/configuration/{CID}" + } + }, + "_embedded": { + "parameters": { + "data": [ + { + "group_id": "{group_id}", + "parameters": [...] + }, + ... + ], + "_links": + "self": { + "href": "/commands/{CID}/parameters" + } + } + } + } + + - GET /commands/{CID}/parameters -- получить параметры указанной команды; + * request: + + null + + * response: + + { + "data": [ + { + "group_id": "{group_id}", + "parameters": [...] + }, + ... + ], + "_links": { + "self": { + "href": "/commands/{CID}/parameters" + } + } + } + +* configs -- совокупность объектов воркеров, находящихся в состоянии + конфигурации. + + - POST /configs/{command} -- создать конфигурацию; + 201 -- конфигурация уже создана; + 404 -- команды с указанным id нет; + 400 -- неправильный формат запроса. + * request: + + null + + * response: + + { + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "execute": { + "href": "/executions/{WID}" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + - PATCH /configs/{WID}/parameters -- изменить конфигурацию + указанными в теле значениями; + 404 -- конфигурации с указанным + WID нет; + 400 -- неправильный формат + запроса. + * request: + + [ + {"id": "{param_id}", "value": "{param_value}"}, + ... + ] + + * response: + + { + "data": { + "{param_id}": "{error_message}", + ... + }, + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + OR + + { + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "execute": { + "href": "/executions/{WID}" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + - PUT /configs/{WID}/parameters -- заменить конфигурацию с + использованием указанных в теле + запроса значений. Пустой набор + значений позволяет сбросить + конфигурацию; + * request: + + [ + {"id": "{param_id}", "value": "{param_value}"}, + ... + ] + + * response: + + { + "data": { + "{param_id}": "{error_message}", + ... + }, + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + OR + + { + "_links": { + "configure": { + "href": "/configs/{WID}/parameters" + }, + "execute": { + "href": "/executions/{WID}" + }, + "cancel": { + "href": "/configs/{WID}" + } + } + } + + - DELETE /configs/{WID} -- закончить конфигурации без выполнения + команды. + * request: + + null + + * response: + + [ + { + "data": { + "type": "output", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}", + } + }, + ... + ] + +* executions -- совокупность объектов воркеров, находящихся в состоянии + исполнения. + + - POST /executions/{WID} -- создать новое исполнение из конфигурации; + 201 -- исполнение создано; + 409 -- исполнение с этим WID уже есть; + 400 -- неправильный формат запроса. + 404 -- конфигурация не найдена. + * request: + + null + + * response: + + - Response Code: 201 + + { + "_links": { + "output": { + "href": "/executions/{WID}/output" + }, + "stop": { + "href": "/executions/{WID}" + } + } + } + + - DELETE /executions/{WID} -- удалить исполнение, происходит путем + отправки в скрипт SIGINT; + * request: + + null + + * response (возвращает список непереданных сообщений, если они есть): + + [ + { + "data": { + "type": "output", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}", + } + }, + ... + ] + + - GET /executions/{WID}/output -- получить список имеющихся сообщений от + команды; + * request: + + null + + * response (необходимость остановить выполнение определяется по + Response Code): + + [ + { + "data": { + "type": "output", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}", + } + }, + { + "data": { + "type": "input", + "text": "{input_message}" + "source": "{source_script}" + }, + "_links": { + "input": { + "href": "/executions/{WID}/input", + } + } + } + ] + + - PATCH /executions/{WID}/input -- отправить в воркер некоторый ввод; + * request (если null, значит ввод прерывается): + + { + "data": {input_data/null} + } + + * response: + - Response Code: 200 + + null + + - Response Code: 404 + + { + "error": "Execution {WID} is not found" + } + + - Response Code: 400 + + { + "error": "Input message is not correct" + } + +* workers -- вся совокупность воркеров. + + - GET /workers/ -- получить список демонов-воркеров и информацию о них; + * request: + + null + + * response: + + { + "{WID}": { + "data": { + "socket": "{socket_path}", + "command": "{worker_command}", + "pid": "{daemon_pid}", + } + "_links": { + "kill": { + "href": "/workers/{WID}", + "methods": ["delete"] + } + } + }, + ... + } + + - DELETE /workers/{WID} -- отправить сигнал SIGKILL демону-воркеру; + * request: + + null + + * response (сообщения полученные от убитого воркера): + + [ + { + "data": { + "type": "output", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}", + } + }, + ... + ] + + +Протокол взаимодействия воркера и сервера +----------------------------------------- +Все сообщения представляют собой json-структуры, включающие в себя 3 поля: + - state -- состояние воркера: + * Если сообщение пришло воркеру от сервера, тогда это поле указывает + воркеру, какое состояние он должен принять; + * Если сообщение пришло серверу от воркера, тогда это поле указывает на + текущее состояние воркера. + + - status -- статус текущего состояния: + 0 -- состояние корректно; + 1 -- состояние некорректно. + + - data -- некоторые данные, структура которых может быть любой. + +1. Данные конфигурации от клиента. + a. Продолжить конфигурацию указанными значениями: + + { + "state": "config", + "status": 0, + "data": [ + {"id": "{param_id}", "value": "{parameter_value}"}, + ... + ] + } + + b. Сбросить конфигурацию и продолжить указанными значениями: + + { + "state": "config", + "status": 1, + "data": [ + {"id": "{param_id}", "value": "{parameter_value}"}, + ... + ] + } + +2. Ответ воркера по данным конфигурации. + a. Все параметры конфигурации корректны: + + { + "state": "config", + "status": 0, + "data": null + } + + b. Некоторые параметры конфигурации некорректны: + + { + "state": "config", + "status": 1, + "data": { + "{param_id}": "{error_message}", + ... + } + } + +3. Запрос на окончание конфигурации от клиента. + a. Закончить конфигурацию, начать выполнение команды: + + { + "state": "finish", + "status": 0, + "data": null + } + + b. Закончить конфигурацию и работу воркера: + + { + "state": "finish", + "status": 1, + "data": null + } + +4. Ответ воркера на запрос об окончании конфигурации: + a. Закончить конфигурацию, начать выполнение команды: + + { + "state": "finish", + "status": 0, + "data": null + } + + b. Закончить конфигурацию и работу воркера: + + { + "state": "finish", + "status": 1, + "data": { + "{param_id}": "{error_message}", + ... + } + } + +5. Сообщение с выводом воркера. + a. Сообщение из скрипта при его "штатной" работe: + + { + "state": "exec", + "status": 0, + "data": { + "type": "message", + "logging": {logging_level/null}, + "text": "{message_text}", + "source": "{source_script}" + } + } + + b. Сообщение об ошибке приводящей к завершению выполнения скрипта и + команды: + + { + "state": "exec", + "status": 1, + "data": { + "error": "{error_message}", + "source": "{source_script}" + } + } + +6. Сообщение с запросом на ввод. + a. Состояние "input" -- воркер в состоянии ожидания ввода: + { + "state": "input", + "status": 0, + "data": { + "text": "{message_text}", + "source": "{source_script}" + } + } + +7. Сообщение от клиента с данными. + a. Ввод успешно выполнен и передается: + + { + "state": "input", + "status": 0, + "data": "{client_data}" + } + + b. Ввод данных прерван, данных не будет: + + { + "state": "input", + "status": 1, + "data": null + } + +8. Сообщение от воркера о получении ввода. + a. Ввод успешно получен, о чем говорит переход в состояние выполнения со + статусом 0, т.е. без ошибок: + + { + "state": "exec", + "status": 0, + "data": null + } + +9. Сообщение воркера об окончании работы: + a. Успешное окончание работы: + + { + "state": "finish", + "status": 0, + "data": null + } + + b. Работа воркера была прервана или закончилась с ошибками: + + { + "state": "finish", + "status": 1, + "data": [ + "{error_message}", + ... + ] + } diff --git a/calculate/server/ipc_draft/io_module.py b/calculate/server/ipc_draft/io_module.py new file mode 100644 index 0000000..262577c --- /dev/null +++ b/calculate/server/ipc_draft/io_module.py @@ -0,0 +1,100 @@ +import json +import logging +from multiprocessing.connection import Listener, Connection +from typing import Union + + +class WorkerIOError(KeyboardInterrupt): + pass + + +class IOModule: + '''Класс модуля ввода/вывода для воркеров.''' + def __init__(self, listener: Listener, command_name: str): + self._listener: Listener = listener + self._connection: Union[Connection, None] = None + + self._command: str = command_name + self._script: str = "" + + @property + def command(self) -> str: + return self._command + + @property + def script(self) -> str: + return self._script + + @script.setter + def script(self, script: str) -> None: + self._script = script + + def set_debug(self, msg: str) -> None: + self.output(msg, level=logging.DEBUG) + + def set_info(self, msg: str) -> None: + self.output(msg, level=logging.INFO) + + def set_warning(self, msg: str) -> None: + self.output(msg, level=logging.WARNING) + + def set_error(self, msg: str) -> None: + self.output(msg, level=logging.ERROR) + + def set_critical(self, msg: str) -> None: + self.output(msg, level=logging.CRITICAL) + + def output(self, text: str, level: int = logging.INFO) -> None: + '''Метод для отправки серверу вывода с указанием уровня.''' + output_request = {"state": "exec", + "status": 0, + "data": { + "type": "message", + "logging": level, + "text": text, + "source": self._script + } + } + self.send(output_request) + + def input(self, msg: str) -> str: + '''Метод через который возможен ввод данных в скрипт.''' + input_request = {"state": "input", + "status": 0, + "data": {"text": msg, + "source": f"{self.command}:{self.script}"}} + + answer = None + while answer is None: + self.send(input_request) + answer = self.receive() + + return answer['data'] + + def send(self, data: dict) -> None: + '''Метод для отправки данных серверу.''' + if self._connection is None: + raise WorkerIOError("No one is connected now.") + data = json.dumps(data).encode() + self._connection.send_bytes(data) + + def receive(self) -> dict: + '''Метод для получения данных от сервера.''' + if self._connection is None: + raise WorkerIOError("No one is connected now.") + data: bytes = self._connection.recv_bytes() + return json.loads(data.decode()) + + def __enter__(self): + self._connection = self._listener.accept() + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + if exc_type: + print("exc_type =", exc_type) + print("exc_value =", exc_value) + print("exc_traceback =", exc_traceback) + if exc_type is KeyboardInterrupt: + print("correctly") + if not self._connection.closed: + self._connection.close() diff --git a/calculate/server/ipc_draft/schemas.py b/calculate/server/ipc_draft/schemas.py new file mode 100644 index 0000000..ec313fc --- /dev/null +++ b/calculate/server/ipc_draft/schemas.py @@ -0,0 +1,202 @@ +from typing import Any, List, Dict, Optional, Literal, Union +from pydantic import BaseModel, Field, HttpUrl +# from utils import ResponseStructure +# from pprint import pprint +# import json +# from fastapi import HTTPException, status + + +class NotTemplatedLinkData(BaseModel): + href: HttpUrl = "URI" + + class Config: + extra = "forbid" + + +class TemplatedLinkData(NotTemplatedLinkData): + templated: bool = False + + +LinkData = Union[NotTemplatedLinkData, TemplatedLinkData] + + +# GET / +class ServerInfo(BaseModel): + name: str = "Server name" + version: str = "Server version" + + +class GetRootResponse(BaseModel): + data: ServerInfo + links: Dict[str, LinkData] = Field(..., alias="_links", + description="Links to resources") + + +# GET /commands/{CID}/parameters +class ParameterInfo(BaseModel): + id: str + default: Any + + +class ParametersGroup(BaseModel): + group_id: str + parameters: List[ParameterInfo] + + +class GetCommandParametersResponse(BaseModel): + data: List[ParametersGroup] + links: Dict[str, LinkData] = Field(..., alias="_links") + + +# GET /commands/{CID} +class CommandData(BaseModel): + id: str + title: str + category: str + command: str + + +class CommandInfo(BaseModel): + data: CommandData + links: Dict[str, LinkData] = Field(..., alias="_links") + + +class FoundCommandInfo(CommandInfo): + embedded: Dict[str, + GetCommandParametersResponse] = Field(..., + alias="_embedded") + + +# GET /commands/ +class GetCommandsResponse(BaseModel): + __root__: Dict[str, CommandInfo] + + # class Config: + # @staticmethod + # def schema_extra(schema, model): + # example = {} + # for num in range(2): + # resp = ResponseStructure() + # resp.add_data(id=f"command_{num}", + # command=f"cl-command-{num}", + # title=f"Command {num}", + # category="cool-category" + # ) + # resp.add_link("parameters", + # f"/commands/command_{num}/parameters") + # resp.add_link("configure", f"/configs/command_{num}") + + # example[f"command_{num}"] = resp.get_dict() + # schema["example"] = example + + +# POST /configs/{command} +class CreateConfigResponse(BaseModel): + links: Dict[str, LinkData] = Field(..., alias="_links") + + +# PATCH /configs/{WID}/parameters +class ParameterValue(BaseModel): + id: str + value: Any + + +class ModifyConfigRequest(BaseModel): + __root__: List[ParameterValue] + + +class ModifyConfigCorrectResponse(BaseModel): + links: Dict[str, LinkData] = Field(..., alias="_links") + + class Config: + extra = "forbid" + + +class ModifyConfigUncorrectResponse(ModifyConfigCorrectResponse): + data: Optional[Dict[str, str]] + links: Dict[str, LinkData] = Field(..., alias="_links") + + +ModifyConfigResponse = Union[ModifyConfigCorrectResponse, + ModifyConfigUncorrectResponse] + + +# PUT /configs/{WID}/parameters +class ResetConfigRequest(BaseModel): + __root__: List[ParameterValue] + + +class ResetConfigResponse(BaseModel): + data: Optional[Dict[str, str]] + links: Dict[str, LinkData] = Field(..., alias="_links") + + +# POST /executions/{WID} +class CreateExecutionResponse(BaseModel): + links: Dict[str, LinkData] = Field(..., alias="_links") + + +# DELETE /executions/{WID} +class OutputData(BaseModel): + type: Literal["output"] + logging: Union[int, None] + text: str + source: str + + +class OutputMessage(BaseModel): + data: OutputData + + +class StopExecutionsResponse(BaseModel): + __root__: List[OutputMessage] + + +# DELETE /configs/{WID} +class StopConfigResponse(BaseModel): + __root__: List[OutputMessage] + + +# GET /executions/{WID}/output +class InputRequestData(BaseModel): + type: Literal["input"] + text: str + source: str + + +class InputMessage(BaseModel): + data: InputRequestData + links: Dict[str, LinkData] = Field(..., alias="_links") + + +class GetExecutionOutputResponse(BaseModel): + __root__: List[Union[OutputMessage, InputMessage]] + + +# PATCH /executions/{WID}/input +class WriteToExecutionRequest(BaseModel): + data: Any + + +# GET /workers/ +class WorkerData(BaseModel): + socket: str + command: str + pid: int + + +class WorkerInfo(BaseModel): + data: WorkerData + links: Dict[str, LinkData] = Field(..., alias="_links") + + +class GetWorkersResponse(BaseModel): + __root__: Dict[int, WorkerInfo] + +# DELETE /workers/{WID} + + +# Сообщения протокола взаимодействия сервера и воркеров. +class WorkerMessageBase(BaseModel): + state: str + status: int diff --git a/calculate/server/ipc_draft/scripts.py b/calculate/server/ipc_draft/scripts.py new file mode 100644 index 0000000..8287ea5 --- /dev/null +++ b/calculate/server/ipc_draft/scripts.py @@ -0,0 +1,55 @@ +from io_module import IOModule +import time + + +def script_0(io: IOModule, parameters: dict) -> None: + try: + io.set_info(f"PARAMETERS: {parameters}") + value_0 = get_integer(0, parameters, io) + value_1 = get_integer(1, parameters, io) + + time.sleep(2) + result = value_0 + value_1 + + io.set_info(f"Script executed with result = {result}") + except Exception as error: + io.set_error(f"{str(error)}") + + +def script_1(io: IOModule, parameters: dict) -> None: + try: + io.set_info(f"PARAMETERS: {parameters}") + value_0 = get_integer(0, parameters, io) + value_1 = get_integer(1, parameters, io) + + time.sleep(3) + result = value_0 * value_1 + + io.set_info(f"Script executed with result = {result}") + except Exception as error: + io.set_error(f"{str(error)}") + + +def script_2(io: IOModule, parameters: dict) -> None: + try: + io.set_info(f"PARAMETERS: {parameters}") + value_0 = get_integer(0, parameters, io) + value_1 = get_integer(1, parameters, io) + + time.sleep(4) + result = value_0 ** value_1 + + io.set_info(f"Script executed with result = {result}") + except Exception as error: + io.set_error(f"{str(error)}") + + +def get_integer(index: int, parameters: dict, io: IOModule) -> int: + io.set_info(f"Looking for 'value_{index}'") + value = parameters.get(f"value_{index}", None) + if value is None: + io.set_warning(f"'value_{index}' not found") + value = io.input(f"Enter 'value_{index}'") + else: + io.set_info(f"value_{index} = {value}") + return int(value) diff --git a/calculate/server/ipc_draft/server.py b/calculate/server/ipc_draft/server.py new file mode 100644 index 0000000..0cf4202 --- /dev/null +++ b/calculate/server/ipc_draft/server.py @@ -0,0 +1,282 @@ +import asyncio + +# import logging +import uvicorn + +from starlette.responses import JSONResponse +from starlette.requests import Request + +from fastapi import FastAPI, status, HTTPException +from fastapi.encoders import jsonable_encoder + +from pydantic.main import ModelMetaclass +from pydantic import parse_obj_as + +from typing import _GenericAlias, Union, Optional + +from commands import command_0, command_1, command_2 +from utils import ResponseStructure, Commands +from worker import ( + WorkersManager, + UnexpectedWorkerFinish, + UnexpectedWorkerState, + DaemonIsDead + ) + +from schemas import ( + GetRootResponse, + GetCommandsResponse, + FoundCommandInfo, + GetCommandParametersResponse, + CreateConfigResponse, + ModifyConfigRequest, + ModifyConfigResponse, + ResetConfigRequest, + ResetConfigResponse, + StopConfigResponse, + CreateExecutionResponse + ) + +from pprint import pprint + + +api = FastAPI(title="Calculate Server") +workers = WorkersManager(loop=asyncio.get_event_loop()) + + +commands = Commands(command_0, + command_1, + command_2) + + +@api.on_event("startup") +async def startup_event(): + check_pid_files() + + +@api.on_event("shutdown") +async def shutdown_event(): + workers.clean() + + +@api.get("/", response_model=GetRootResponse, tags=["Home"]) +async def get_primary_commands(request: Request): + resp = ResponseStructure(get_base_url(request)) + resp.add_data(name="Calculate Server", version="0.1") + resp.add_link("commands", "/commands?gui={is_gui}", templated=True) + resp.add_link("find_command", "/commands/{console_command}?gui={is_gui}", + templated=True) + resp.add_link("workers", "/workers") + return validate_response(resp.get_dict(), GetRootResponse, + media_type="application/hal+json") + + +@api.get("/commands", response_model=GetCommandsResponse, tags=["Commands"]) +async def get_available_commands(request: Request, + gui: Optional[bool] = False): + response_data = commands.get_commands(get_base_url(request)) + return validate_response(response_data, GetCommandsResponse, + media_type="application/hal+json") + + +@api.get("/commands/{command}", + response_model=FoundCommandInfo, + tags=["Commands"]) +async def find_command_data(command: str, request: Request, + gui: Optional[bool] = False, + by_id: Optional[bool] = False): + base_url = get_base_url(request) + + if by_id: + command_data = commands.get_by_id(command, base_url) + if command_data is None: + raise get_command_not_found(command) + else: + command_data = commands.find_command(command, base_url) + if command_data is None: + raise get_cl_command_not_found(command) + + return validate_response(command_data, FoundCommandInfo, + media_type="application/hal+json") + + +@api.get("/commands/{command_id}/parameters", + response_model=GetCommandParametersResponse, + tags=["Commands"]) +async def get_command_parameters(command_id: str, request: Request): + parameters_data = commands.get_parameters(command_id, + get_base_url(request)) + if parameters_data is None: + raise get_command_not_found(command_id) + return parameters_data + + +@api.post("/configs/{command_id}", response_model=CreateConfigResponse, + tags=["Configurations"]) +async def create_configuration(command_id: str, request: Request): + command = commands.get_command_object(command_id) + if command is None: + raise get_command_not_found(command_id) + + print("Making worker") + worker_id, error = await workers.make_worker(command) + if error is not None: + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=error) + + resp = ResponseStructure(get_base_url(request)) + resp.add_link("configure", f"/configs/{worker_id}/parameters") + resp.add_link("execute", f"/execs/{worker_id}") + resp.add_link("cancel", f"/configs/{worker_id}") + + return validate_response(resp.get_dict(), CreateConfigResponse) + + +@api.patch("/configs/{wid}/parameters", tags=["Configurations"]) +async def modify_configuration_parameters(wid: int, + parameters: ModifyConfigRequest, + request: Request): + response = await change_config(get_base_url(request), wid, parameters) + return validate_response(response, ModifyConfigResponse) + + +@api.put("/configs/{wid}/parameters", tags=["Configurations"]) +async def reset_configuration_parameters(wid: int, + parameters: ResetConfigRequest, + request: Request): + response = await change_config(get_base_url(request), wid, parameters, + reset=True) + return validate_response(response, ResetConfigResponse) + + +async def change_config(base_url: str, wid: int, + parameters: ModifyConfigRequest, + reset: bool = False) -> dict: + parameters = parameters.dict()["__root__"] + try: + result = await workers.configure_worker(wid, parameters, reset=reset) + except UnexpectedWorkerFinish as error: + return HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=("Unexpected configuration finish. Reason:" + f" {error.reason[0]}")) + except UnexpectedWorkerState as error: + print("Unexpected worker state.") + return HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=('Unexpected configuration state:' + f' "{error.state}". Expected:' + f' "{error.expected}"')) + except DaemonIsDead: + print("DAEMON IS DEAD") + return HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Daemon is dead") + print("RESULT:") + pprint(result) + if result is None: + raise get_configuration_not_found(wid) + + resp = ResponseStructure(base_url) + resp.add_link("configure", f"configs/{wid}/parameters") + resp.add_link("reset", f"configs/{wid}/parameters") + resp.add_link("cancel", f"configs/{wid}") + + if not result: + resp.add_link("execute", f"/execs/{wid}") + else: + resp.add_data(result) + + return resp.get_dict() + + +@api.delete("/configs/{wid}", response_model=StopConfigResponse, + tags=["Configurations"]) +async def stop_configuration(wid: int): + output = await workers.cancel_worker_configuration(wid) + if output is None: + raise get_configuration_not_found(wid) + return [] + + +@api.post("/execs/{wid}", tags=["Executions"]) +async def start_command_execution(wid: int, request: Request): + result = workers.run_execution(wid) + if result > 0: + raise get_configuration_not_found(wid) + elif result < 0: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, + detail=f"Execution id = {wid} already exists.") + + resp = ResponseStructure(get_base_url(request)) + resp.add_link("configure", f"/configs/{wid}/parameters") + resp.add_link("reset", f"/configs/{wid}/parameters") + resp.add_link("cancel", f"/configs/{wid}") + + return validate_response(resp.get_dict(), CreateExecutionResponse) + + +@api.delete("/execs/{wid}", tags=["Executions"]) +async def stop_command_execution(wid: int): + raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="'stop execution' is not implemented") + + +@api.get("/execs/{wid}/output", tags=["Executions"]) +async def read_execution_output(wid: int): + raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="'stop execution' is not implemented") + + +@api.patch("/execs/{wid}/input", tags=["Executions"]) +async def write_to_execution(wid: int): + raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="'write to execution' is not implemented") + + +def check_pid_files(pid_files_dir: str = "./") -> None: + pass + + +def get_base_url(request: Request): + base_url = f"{request.base_url.scheme}://{request.base_url.netloc}" + return base_url + + +def get_command_not_found(command_id: str) -> HTTPException: + return HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=(f'Command with id "{command_id}"' + ' is not found.')) + + +def get_cl_command_not_found(console_command: str) -> HTTPException: + return HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=(f'Console command "{console_command}" is not' + ' found.')) + + +def get_configuration_not_found(wid: int) -> HTTPException: + return HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=f"Configuration with id={wid} is not found.") + + +def validate_response(data: Union[dict, list], + schema: Union[ModelMetaclass, _GenericAlias, type], + media_type: Optional[str] = None, + status_code: int = 200 + ) -> JSONResponse: + """Функция для валидации данных ответа сервера по указанной схеме с учетом + наличия псевдонимов полей и возможности того, что схема задана обычным + типом, типом из typing или BaseModel с __root__. Возвращает объект ответа. + """ + if isinstance(schema, ModelMetaclass): + if "__root__" in schema.__annotations__: + validated = schema(__root__=data) + else: + validated = schema(**data) + elif isinstance(schema, (_GenericAlias, type)): + validated = parse_obj_as(schema, data) + + return JSONResponse(content=jsonable_encoder(validated, by_alias=True), + media_type=media_type, status_code=status_code) + + +if __name__ == "__main__": + uvicorn.run("server:api", host="127.0.0.1", port=2007, reload=True) diff --git a/calculate/server/ipc_draft/utils.py b/calculate/server/ipc_draft/utils.py new file mode 100644 index 0000000..f7711e7 --- /dev/null +++ b/calculate/server/ipc_draft/utils.py @@ -0,0 +1,199 @@ +from fastapi import status, HTTPException +from fastapi.encoders import jsonable_encoder + +from starlette.responses import JSONResponse +from starlette.requests import Request + +from pydantic.main import ModelMetaclass +from pydantic import parse_obj_as + +from typing import Dict, Union, List, _GenericAlias, Optional + +from commands import Command +# from pprint import pprint + + +class ResponseStructure: + '''Класс структуры данных, для получения HATEAOS ответа на запросы + клиента.''' + def __init__(self, base_url: str): + self._base_url: str = base_url.strip("/") + + self._data: dict = dict() + self._links: Dict[str, Dict[str, str]] = dict() + self._embedded: Dict[str, dict] = dict() + + def get_dict(self) -> dict: + dictionary = dict() + if self._data: + dictionary["data"] = self._data + if self._links: + dictionary["_links"] = self._links + if self._embedded: + dictionary["_embedded"] = self._embedded + return dictionary + + def add_data(self, *args, **kwargs) -> "ResponseStructure": + if args: + self._data = args[0] + if kwargs: + self._data.update(kwargs) + return self + + def embed(self, resource: str, + content: Union[dict, list]) -> "ResponseStructure": + self._embedded[resource] = content + return self + + def add_link(self, action: str, uri: str, + templated: bool = False) -> "ResponseStructure": + uri = self._get_uri(uri) + + if action in self._links: + link_section = self._links[action] + if link_section["href"] != uri: + link_section["href"] = uri + else: + link_section = {"href": uri} + self._links[action] = link_section + + if templated: + print("TEMPLATED:", action) + link_section["templated"] = templated + + return self + + def _get_uri(self, uri: str) -> str: + if uri.startswith(self._base_url): + return uri + return f"{self._base_url}/{uri.strip('/')}" + + +class Commands: + '''Предварительная реализация контейнера описаний команд.''' + def __init__(self, *commands: List[Command]): + self._by_id: dict = dict() + self._by_command: dict = dict() + for command in commands: + self._by_id[command.id] = command + self._by_command[command.command] = command + + def get_commands(self, base_url: str) -> Dict[str, dict]: + response = dict() + + for command in self._by_id.values(): + data = ResponseStructure(base_url) + data.add_data(id=command.id, + title=command.title, + category=command.category, + command=command.command) + data.add_link("self", f"/commands/{command.id}?by_id=true") + data.add_link("parameters", f"/commands/{command.id}/parameters") + data.add_link("configure", f"/configs/{command.id}") + response[command.id] = data.get_dict() + + return response + + def get_by_id(self, command_id: str, base_url: str) -> Dict[str, Dict]: + if command_id in self._by_id: + command = self._by_id[command_id] + data = ResponseStructure(base_url) + data.add_data(id=command.id, + title=command.title, + category=command.category, + command=command.command) + data.add_link("self", f"/commands/{command.id}?by_id=1") + data.add_link("parameters", f"/commands/{command.id}/parameters") + data.add_link("configure", f"/configs/{command.id}") + parameters_data = self._get_parameters_data(command.parameters, + command.id, base_url) + data.embed("parameters", parameters_data) + return data.get_dict() + + return None + + def find_command(self, console_command: str, + base_url: str) -> Union[dict, None]: + if console_command in self._by_command: + command = self._by_command[console_command] + + data = ResponseStructure(base_url) + data.add_data(id=command.id, + title=command.title, + category=command.category, + command=command.command) + data.add_link("self", f"/commands/{command.id}?by_id=1") + data.add_link("parameters", f"/commands/{command.id}/parameters") + data.add_link("configure", f"/configs/{command.id}") + parameters_data = self._get_parameters_data(command.parameters, + command.id, base_url) + data.embed("parameters", parameters_data) + + return data.get_dict() + else: + return None + + def get_parameters(self, command_id: str, base_url: str + ) -> Union[dict, None]: + if command_id in self._by_id: + command = self._by_id[command_id] + parameters_data = self._get_parameters_data(command.parameters, + command.id, base_url) + return parameters_data + else: + return None + + def get_command_object(self, command_id: str) -> Command: + if command_id in self._by_id: + return self._by_id[command_id] + return None + + def _get_parameters_data(self, parameters: list, command_id: str, + base_url: str) -> List[dict]: + data = ResponseStructure(base_url) + data.add_data(parameters) + data.add_link("self", f"/commands/{command_id}/parameters") + return data.get_dict() + + +def get_base_url(request: Request): + base_url = f"{request.base_url.scheme}://{request.base_url.netloc}" + return base_url + + +def get_command_not_found(command_id: str) -> HTTPException: + return HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=(f'Command with id "{command_id}"' + ' is not found.')) + + +def get_cl_command_not_found(console_command: str) -> HTTPException: + return HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=(f'Console command "{console_command}" is not' + ' found.')) + + +def get_configuration_not_found(wid: int) -> HTTPException: + return HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=f"Configuration with id={wid} is not found.") + + +def validate_response(data: Union[dict, list], + schema: Union[ModelMetaclass, _GenericAlias, type], + media_type: Optional[str] = None, + status_code: int = 200 + ) -> JSONResponse: + """Функция для валидации данных ответа сервера по указанной схеме с учетом + наличия псевдонимов полей и возможности того, что схема задана обычным + типом, типом из typing или BaseModel с __root__. Возвращает объект ответа. + """ + if isinstance(schema, ModelMetaclass): + if "__root__" in schema.__annotations__: + validated = schema(__root__=data) + else: + validated = schema(**data) + elif isinstance(schema, (_GenericAlias, type)): + validated = parse_obj_as(schema, data) + + return JSONResponse(content=jsonable_encoder(validated, by_alias=True), + media_type=media_type, status_code=status_code) diff --git a/calculate/server/ipc_draft/worker.py b/calculate/server/ipc_draft/worker.py new file mode 100644 index 0000000..626947e --- /dev/null +++ b/calculate/server/ipc_draft/worker.py @@ -0,0 +1,413 @@ +import os +import sys +import json +import struct +import signal +import asyncio +import logging +from multiprocessing import Process +from multiprocessing.connection import Listener +# import logging +# import time +from daemon import daemon +from commands import Command + +from typing import Dict, Optional, List, Union, Tuple, Literal + +from asyncio.unix_events import _UnixSelectorEventLoop +from asyncio import get_event_loop + + +LOG_LEVELS = {logging.ERROR: "ERROR", + logging.INFO: "INFO", + logging.WARNING: "WARNING", + logging.DEBUG: "DEBUG", + logging.CRITICAL: "CRITICAL", + } + + +class WorkerException(Exception): + pass + + +class UnexpectedWorkerFinish(WorkerException): + def __init__(self, reason: List[str], output: Optional[List[str]] = []): + """Исключение кидаемое в случае, если воркер неожиданно умер.""" + self.reason: List[str] = reason + self.output: List[dict] = output + + +class UnexpectedWorkerState(WorkerException): + """Исключение кидаемое в случае, если состояние воркера не соответствует + ожидаемому.""" + def __init__(self, state: str, expected: str): + self.state: str = state + self.expected: str = expected + + +class DaemonIsDead(WorkerException): + """Исключение кидаемое в случае, если демон-воркер неожиданно оказался + мертв.""" + def __init__(self, info: str, output: Optional[List[str]] = []): + self.info: str = info + self.output: List[dict] = output + + +class IncorrectConfiguration(WorkerException): + """Исключение кидаемое при попытке запустить выполнение конфигурации, в + которой есть ошибки.""" + def __init__(self, errors: Dict[str, str]): + self._errors = errors + + +class WorkersManager: + def __init__(self, loop: Optional[_UnixSelectorEventLoop] = None): + if loop is None: + self._loop = get_event_loop() + else: + self._loop = loop + + self._configs: Dict[int, WorkerProtocol] = {} + self._execs: Dict[int, WorkerProtocol] = {} + self._wids: List[int] = [] + + async def make_worker(self, command: Command) -> Tuple[Union[int, None], + Union[str, None]]: + """Метод для создания воркеров.""" + wid = self._get_wid() + try: + # Создаем воркера и получаем интерфейс для взаимодействия с ним. + worker_api = await self._create_worker(wid, command) + self._configs[wid] = worker_api + except Exception as error: + if wid in self._configs: + self._configs.pop(wid) + return None, str(error) + return wid, None + + async def configure_worker(self, wid: int, parameters: List[dict], + reset: bool = False) -> Union[dict, None]: + """Метод для модификации или сброса параметров конфигурации указанными + значениями. + """ + if not parameters: + return {} + if wid not in self._configs: + return None + + worker_api: WorkerProtocol = self._configs[wid] + try: + result = await worker_api.configure(parameters) + except (UnexpectedWorkerFinish, DaemonIsDead): + worker_api.kill() + self._configs.pop(wid) + raise + + return result + + async def cancel_worker_configuration(self, wid: int): + """Метод для завершения конфигурации воркера и завершения его работы. + """ + if wid not in self._configs: + return None + + worker_api: WorkerProtocol = self._configs[wid] + output = await worker_api.cancel_configuration() + worker_api.delete_socket() + self._configs.pop(wid) + + return output + + def clean(self): + """Метод для убийства всех работающих на данный момент демонов.""" + # TODO доработать его логику так, чтобы при перезапуске сервера этот + # метод никого не убивал. + for wid, worker in {**self._configs, **self._execs}.items(): + result = False + try: + result = worker.kill() + finally: + if result: + print(f"[*] Worker {wid} is killed.") + else: + print(f"[x] Worker {wid} is not killed.") + continue + + def get_config(self, wid: int): + if wid in self._configs: + return self._configs[wid] + return None + + def get_execution(self, wid: int): + if wid in self._execs: + return self._execs[wid] + return None + + def run_execution(self, wid: int) -> Tuple[int, Union[dict, None]]: + '''Метод для запуска исполнения команды, используя указанную + конфигурацию. + Вывод: + 0, None -- исполнение успешно запущено; + 1, None -- конфигурация с данным wid не найдена; + 2, errors -- конфигурация с данным wid содержит ошибки; + 3, None -- исполнение с данным wid уже существует; + ''' + if wid not in self._configs: + return 1, None + if self._configs[wid].status > 0: + # Берем ошибки, как-то. + errors = {} + return 2, errors + if wid in self._execs: + return 3, None + + # worker = self._configs.pop(wid) + return 0, None + + async def _create_worker(self, wid: int, command: Command + ) -> "WorkerProtocol": + # Путь к временному файлу, в котором будет лежать pid демона-рабочего. + pid_file_path: str = os.path.join(os.getcwd(), f"pid_file_{wid}") + socket_path: str = os.path.join(os.getcwd(), + f"worker_{wid}.sock") + if os.path.exists(socket_path): + os.remove(socket_path) + + process_runner = Process(target=daemonize_worker, + args=(socket_path, wid, pid_file_path, + command) + ) + process_runner.start() + process_runner.join() + + daemon_pid = await self._get_daemon_pid(pid_file_path) + self._delete_pid_file(pid_file_path) + + transport, worker_interface = await self._loop.create_unix_connection( + lambda: WorkerProtocol(daemon_pid, + socket_path), + socket_path) + return worker_interface + + def _get_wid(self): + if not self._wids: + worker_id = 0 + else: + worker_id = self._wids[-1] + 1 + self._wids.append(worker_id) + return worker_id + + async def _get_daemon_pid(self, pid_file_path: str) -> int: + while True: + if not os.path.exists(pid_file_path): + # Если файла нет -- уступаем работу другим воркерам. + await asyncio.sleep(0) + with open(pid_file_path, "r") as pid_file: + daemon_pid = int(pid_file.readline()) + return daemon_pid + + def _delete_pid_file(self, pid_file_path: str) -> None: + try: + os.remove(pid_file_path) + except OSError: + pass + + def __repr__(self): + configs = ["Configurations:"] + configs.extend([f"wid={wid}" for wid in self._configs.keys()]) + configs_repr = "\n\t".join(configs) + + execs = ["Executions:"] + execs.extend([f"wid={wid}" for wid in self._execs.keys()]) + execs_repr = "\n\t".join(execs) + + return f"{configs_repr}\n{execs_repr}" + + +def daemonize_worker(socket_path: str, wid: int, pid_file: str, + command: Command) -> None: + '''Функция запускаемая в отдельном процессе и порождающая демона при помощи + магии двойного форка.''' + base_dir = os.getcwd() + + new_fork() + + os.chdir("/") + os.setsid() + os.umask(0) + + new_fork() + + # Создаем файл, с помощью которого будем передавать pid демона-рабочего. + daemon_pid = str(os.getpid()) + with open(pid_file, "w") as pid_file: + pid_file.write(daemon_pid) + + with Listener(socket_path) as listener: + daemon(listener, wid, base_dir, command) + + +def new_fork() -> None: + '''Функция для создания форка текущего процесса.''' + try: + middle_pid = os.fork() + if middle_pid > 0: + sys.exit(0) + except OSError as error: + print(f"FORK ERROR: {error.errno} - {str(error)}") + sys.exit(1) + + +class WorkerProtocol(asyncio.Protocol): + """Класс протокола управления воркером.""" + def __init__(self, pid: int, socket_path: str): + self.loop = asyncio.get_running_loop() + + self._pid: int = pid + self._socket_path: str = socket_path + + # Объект Event, предназначенный для остановки записи при переполнении + # буффера записи в трaнспорте. + self._can_write = asyncio.Event() + self._can_write.clear() + + self._msg_queue = asyncio.Queue() + + # Контекст чтения данных воркера. + self._buffer = bytearray() + self._msg_size: int = 0 + + self._connection_closed: bool = True + + async def configure(self, parameters: List[dict], + reset: bool = False) -> list: + """Метод для конфигурации воркера указанными значениями параметров. + Возвращает список ошибок, если таковые есть, или пустой список, если + никаких ошибок нет. + """ + message = self._make_worker_message("config", int(reset), parameters) + await self._write(message) + response = await self._read() + response = json.loads(response) + + return response["data"] + + async def cancel_configuration(self) -> List[dict]: + """Метод для остановки конфигурации воркера и его работы в целом.""" + stop_message: bytes = self._make_worker_message("finish", 1, None) + await self._write(stop_message) + response = await self._read() + response = json.loads(response) + + return response["data"] + + def connection_made(self, transport: asyncio.Transport) -> None: + self.transport = transport + self._can_write.set() + self._connection_closed: bool = False + print("Connection made.") + + def connection_lost(self, exc: BaseException) -> None: + if exc: + print("Connection lost. Error:", str(exc)) + else: + print("Connection lost.") + self._connection_closed: bool = True + + def data_received(self, data: bytes) -> None: + self._buffer.extend(data) + + while True: + if not self._msg_size: + if len(self._buffer) < 4: + return + size_length = struct.calcsize(">i") + self._msg_size, = struct.unpack(">i", + self._buffer[:size_length]) + self._buffer = self._buffer[size_length:] + + if len(self._buffer) >= self._msg_size: + data = self._buffer[:self._msg_size] + self._buffer = self._buffer[self._msg_size:] + self._msg_size = 0 + + self._msg_queue.put_nowait(data.decode()) + else: + break + + def pause_writing(self) -> None: + self._can_write.clear() + + def resume_writing(self) -> None: + self._can_write.set() + + async def _read(self) -> str: + if self._connection_closed and self._msg_queue.empty(): + return None + return await self._msg_queue.get() + + async def read_all(self) -> List[str]: + if self._msg_queue.empty(): + if self._connection_closed: + return None + else: + return await self._msg_queue.get() + output = [] + while not self._msg_queue.empty(): + output.append(self._msg_queue.get_nowait()) + return output + + async def _write(self, data: Union[str, bytes]) -> None: + await self._can_write.wait() + if isinstance(data, str): + data = data.encode() + self.transport.write(struct.pack(">i", len(data))) + self.transport.write(data) + + def _make_worker_message( + self, + state: Literal["config", "finish", "exec", "input"], + status: int, data: Union[list, dict]) -> dict: + return json.dumps({"state": state, + "status": status, + "data": data}).encode() + + def delete_socket(self) -> None: + """Метод для удаления сокета.""" + try: + os.remove(self._socket_path) + self.transport.close() + self._connection_closed = True + except OSError: + pass + + def kill(self) -> bool: + """Метод для убийства демона.""" + print("GONNA KILLA DAEMON:", self._pid) + if self.is_daemon_alive(): + try: + os.kill(self._pid, signal.SIGKILL) + except OSError: + return False + else: + print("ALREADY DEAD") + return True + + def stop(self) -> bool: + print("STOP DAEMON:", self._daemon_pid) + if self.is_daemon_alive(): + try: + # Для остановки демона посылаем в него KeyboardInterrupt. + os.kill(self._pid, signal.SIGINT) + except OSError as error: + print(f"ERROR: Failed stopping daemon PID={self._pid}. " + f"Reason: {str(error)}") + return False + return True + + def is_daemon_alive(self) -> bool: + try: + os.kill(self._pid, 0) + return True + except OSError: + return False diff --git a/calculate/server/ipc_draft/worker_old.py b/calculate/server/ipc_draft/worker_old.py new file mode 100644 index 0000000..b1df927 --- /dev/null +++ b/calculate/server/ipc_draft/worker_old.py @@ -0,0 +1,428 @@ +import os +import sys +import json +import socket +import signal +import logging +from multiprocessing import Process +# from asyncio import sleep +# import logging +# import time +from io_module import IOModule +from daemon import daemon +from commands import Command + +from typing import Dict, Optional, List, Union, Tuple, Literal + +from asyncio.unix_events import _UnixSelectorEventLoop +from asyncio import get_event_loop, sleep + +from pprint import pprint + + +LOG_LEVELS = {logging.ERROR: "ERROR", + logging.INFO: "INFO", + logging.WARNING: "WARNING", + logging.DEBUG: "DEBUG", + logging.CRITICAL: "CRITICAL", + } + + +class WorkerException(Exception): + pass + + +class UnexpectedWorkerFinish(WorkerException): + def __init__(self, reason: List[str], output: Optional[List[str]] = []): + """Исключение кидаемое в случае, если воркер неожиданно умер.""" + self.reason: List[str] = reason + self.output: List[dict] = output + + +class UnexpectedWorkerState(WorkerException): + """Исключение кидаемое в случае, если состояние воркера не соответствует + ожидаемому.""" + def __init__(self, state: str, expected: str): + self.state: str = state + self.expected: str = expected + + +class DaemonIsDead(WorkerException): + """Исключение кидаемое в случае, если демон-воркер неожиданно оказался + мертв.""" + def __init__(self, info: str, output: Optional[List[str]] = []): + self.info: str = info + self.output: List[dict] = output + + +class IncorrectConfiguration(WorkerException): + """Исключение кидаемое при попытке запустить выполнение конфигурации, в + которой есть ошибки.""" + def __init__(self, errors: Dict[str, str]): + self._errors = errors + + +class WorkersManager: + def __init__(self, loop: Optional[_UnixSelectorEventLoop] = None): + if loop is None: + self._loop = get_event_loop() + else: + self._loop = loop + + self._configs: Dict[int, Worker] = {} + self._execs: Dict[int, Worker] = {} + self._wids: List[int] = [] + + async def make_worker(self, command: Command) -> Tuple[Union[int, None], + Union[str, None]]: + wid = self._get_wid() + try: + worker = Worker(wid, loop=self._loop) + await worker.create_daemon(command) + self._configs[wid] = worker + except Exception as error: + if wid in self._configs: + self._configs.pop(wid) + return None, str(error) + return wid, None + + async def configure_worker(self, wid: int, parameters: List[dict], + reset: bool = False) -> Union[dict, None]: + """Метод для модификации или сброса параметров конфигурации указанными + значениями.""" + if not parameters: + return {} + if wid not in self._configs: + return None + + worker: Worker = self._configs[wid] + try: + result = await worker.configure(parameters) + except (UnexpectedWorkerFinish, DaemonIsDead): + worker.kill() + self._configs.pop(wid) + raise + + return result + + async def cancel_worker_configuration(self, wid: int): + if wid not in self._configs: + return None + + worker: Worker = self._configs[wid] + output = await worker.cancel_configuration() + self._configs.pop(wid) + + return output + + def clean(self): + """Метод для убийства всех работающих на данный момент демонов.""" + # TODO доработать его логику так, чтобы при перезапуске сервера этот + # метод никого не убивал. + print("KILLING WORKERS:") + for wid, worker in {**self._configs, **self._execs}.items(): + result = False + try: + result = worker.kill() + finally: + if result: + print(f"[*] Worker {wid} is killed.") + else: + print(f"[x] Worker {wid} is not killed.") + continue + + def get_config(self, wid: int): + if wid in self._configs: + return self._configs[wid] + return None + + def get_execution(self, wid: int): + if wid in self._execs: + return self._execs[wid] + return None + + def run_execution(self, wid: int) -> Tuple[int, Union[dict, None]]: + '''Метод для запуска исполнения команды, используя указанную + конфигурацию. + Вывод: + 0, None -- исполнение успешно запущено; + 1, None -- конфигурация с данным wid не найдена; + 2, errors -- конфигурация с данным wid содержит ошибки; + 3, None -- исполнение с данным wid уже существует; + ''' + if wid not in self._configs: + return 1, None + if self._configs[wid].status > 0: + # Берем ошибки, как-то. + errors = {} + return 2, errors + if wid in self._execs: + return 3, None + + worker = self._configs.pop(wid) + return 0, None + + def _get_wid(self): + if not self._wids: + worker_id = 0 + else: + worker_id = self._wids[-1] + 1 + self._wids.append(worker_id) + return worker_id + + def __repr__(self): + configs = ["Configurations:"] + configs.extend([f"wid={wid}" for wid in self._configs.keys()]) + configs_repr = "\n\t".join(configs) + + execs = ["Executions:"] + execs.extend([f"wid={wid}" for wid in self._execs.keys()]) + execs_repr = "\n\t".join(execs) + + return f"{configs_repr}\n{execs_repr}" + + +class Worker: + def __init__(self, wid: int, + loop: Optional[_UnixSelectorEventLoop] = None): + if loop is None: + self._loop = get_event_loop() + else: + self._loop = loop + + self._wid: int = wid + self._socket_path: str = os.path.join(os.getcwd(), + f"worker_{self._wid}.sock") + if os.path.exists(self._socket_path): + os.remove(self._socket_path) + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._daemon_pid = None + + # Контекст для чтения данных из сокета. + self._context: str = "" + + self._state: str = "" + self._status: int = 0 + + @property + def state(self): + return self._state + + @property + def status(self): + return self._status + + async def configure(self, parameters: List[dict], + reset: bool = False) -> Union[dict, list]: + '''Метод для отправки в демона-воркера занчений параметров во время + конфигурации.''' + if self._state != "config": + raise UnexpectedWorkerState(self._state, "config") + + message = self._make_worker_message("config", int(reset), parameters) + await self.send(message) + + answers = await self.read() + + print("WORKER'S ANSWERS") + pprint(answers) + + config_answer = answers[0] + if config_answer["state"] == "finish": + raise UnexpectedWorkerFinish(config_answer["data"], + output=answers[1:]) + return config_answer["data"] + + async def cancel_configuration(self) -> List[dict]: + if self._state != "config": + raise UnexpectedWorkerState(self._state, "config") + + stop_message = self._make_worker_message("finish", 1, None) + await self.send(stop_message) + + output = await self.read() + + print("OUTPUT") + pprint(output) + if output: + return output + return [] + + async def send(self, data: dict): + await self._loop.run_in_executor(None, self._send, + json.dumps(data) + '\0') + + async def read(self): + messages = await self._loop.run_in_executor(None, self._read) + return messages + + async def create_daemon(self, command: Command) -> None: + # Путь к временному файлу, в котором будет лежать pid демона-рабочего. + pid_file: str = os.path.join(os.getcwd(), f"PID_file_{self._wid}") + + io = IOModule(self._socket_path, command.id) + + runner_process = Process(target=daemonize_worker, + args=(io, self._wid, pid_file, + command) + ) + runner_process.start() + self.sock.connect(self._socket_path) + runner_process.join() + + self._daemon_pid = 0 + while not self._daemon_pid: + await sleep(0.5) + self._daemon_pid = self._get_daemon_pid(pid_file) + self._delete_pid_file(pid_file) + + print("DAEMON PID =", self._daemon_pid) + print("IS_ALIVE:", self.is_daemon_alive()) + self._state = "config" + + def kill(self) -> bool: + """Метод для убийства демона.""" + print("GONNA KILLA DAEMON:", self._daemon_pid) + if self.is_daemon_alive(): + try: + os.kill(self._daemon_pid, signal.SIGKILL) + except OSError: + return False + else: + print("ALREADY DEAD") + # self._delete_socket() + self._daemon_pid = None + return True + + def _stop_daemon(self) -> bool: + print("STOP DAEMON:", self._daemon_pid) + if self.is_daemon_alive(): + try: + # Для остановки демона посылаем в него KeyboardInterrupt. + os.kill(self._daemon_pid, signal.SIGINT) + except OSError as error: + print(f"ERROR: Failed stopping daemon PID={self._daemon_pid}. " + f"Reason: {str(error)}") + return False + return True + + def is_daemon_alive(self) -> bool: + try: + os.kill(self._daemon_pid, 0) + except OSError: + return False + else: + return True + + def _send(self, data: str) -> bool: + try: + self.sock.sendall(data.encode()) + except socket.error as error: + print(f"ERROR: can not send data to worker {self._wid}." + f" Reason: {str(error)}") + return False + return True + + def _read(self) -> Union[dict, None]: + input_queue = [] + while True: + try: + data = self.sock.recv(1024) + except Exception as error: + print(f"Connection is broken. Error: {str(error)}") + return None + raise DaemonIsDead(f"Connection is broken. " + f"Error: {str(error)}") + if data: + # Если данные получены делим их по нулевому байту. + data = data.decode().split('\0') + break + # Иначе считаем, что соединение нарушилось. + print("Connection is broken. Empty data.") + return None + raise DaemonIsDead("Connection is broken.") + + if self._context: + self._context += data.pop(0) + # Если после взятия первого значения еще что-то есть в списке + # данных -- значит среди них был нулевой байт. И из значения + # контекста можно получить прочитанные данные. + if data: + request = json.loads(self._context) + input_queue.append(request) + self._context = "" + + if data: + # Если данные еще есть в списке -- значит последнее значение в + # списке, даже если это пустая строка, является котекстом. + self._context = data.pop() + if data: + # Остальное добавляем в очередь. + for request in data: + input_queue.append(json.loads(request)) + + return input_queue + + def _make_worker_message(self, state: Literal["config", "finish", + "exec", "input"], + status: int, data: Union[list, dict]) -> dict: + return {"state": state, + "status": status, + "data": data} + + def _get_daemon_pid(self, pid_file_path: str) -> int: + if not os.path.exists(pid_file_path): + return 0 + try: + with open(pid_file_path, "r") as pid_file: + daemon_pid = int(pid_file.readline()) + return daemon_pid + except Exception: + return 0 + + def _delete_socket(self) -> None: + """Метод для удаления сокета.""" + try: + self.sock.close() + os.remove(self._socket_path) + except OSError: + pass + + def _delete_pid_file(self, pid_file_path: str) -> None: + try: + os.remove(pid_file_path) + except OSError: + pass + + +def daemonize_worker(io: IOModule, wid: int, pid_file: str, + command: Command) -> None: + '''Функция запускаемая в отдельном процессе и порождающая демона при помощи + магии двойного форка.''' + base_dir = os.getcwd() + + new_fork() + + os.chdir("/") + os.setsid() + os.umask(0) + + new_fork() + + # Создаем файл, с помощью которого будем передавать pid демона-рабочего. + daemon_pid = str(os.getpid()) + with open(pid_file, "w") as pid_file: + pid_file.write(daemon_pid) + + daemon(io, wid, base_dir, command) + + +def new_fork() -> None: + '''Функция для создания форка текущего процесса.''' + try: + middle_pid = os.fork() + if middle_pid > 0: + sys.exit(0) + except OSError as error: + print(f"FORK ERROR: {error.errno} - {str(error)}") + sys.exit(1) diff --git a/calculate/server/routers/commands.py b/calculate/server/routers/commands.py index 7ad00f6..2ae6925 100644 --- a/calculate/server/routers/commands.py +++ b/calculate/server/routers/commands.py @@ -1,56 +1,66 @@ from fastapi import APIRouter, Depends +from starlette.requests import Request +from ..utils.responses import ( + ResponseStructure, + validate_response, + get_base_url, + get_command_not_found, + get_cl_command_not_found, + ) +from typing import Optional -from ..utils.dependencies import right_checkers - +from ..utils.dependencies import get_current_user +from ..utils.users import check_user_rights from ..server_data import ServerData +from ..schemas.users import User + +from ..schemas.responses import ( + GetRootResponse, + GetCommandsResponse, + FoundCommandInfo, + GetCommandParametersResponse, + ) data = ServerData() router = APIRouter() -@router.get("/commands", tags=["Commands management"], - dependencies=[Depends(right_checkers["read"])]) -async def get_commands() -> dict: - '''Обработчик, отвечающий на запросы списка команд.''' - response = {} - for command_id, command_object in data.commands.items(): - response.update({command_id: {"title": command_object.title, - "category": command_object.category, - "icon": command_object.icon, - "command": command_object.command}}) - return response - - -@router.get("/commands/{cid}", tags=["Commands management"], - dependencies=[Depends(right_checkers["read"])]) -async def get_command(cid: int) -> dict: - '''Обработчик запросов списка команд.''' - if cid not in data.commands_instances: - # TODO добавить какую-то обработку ошибки. - pass - return {'id': cid, - 'name': f'command_{cid}'} - - -@router.get("/commands/{cid}/groups", tags=["Commands management"], - dependencies=[Depends(right_checkers["read"])]) -async def get_command_parameters_groups(cid: int) -> dict: - '''Обработчик запросов на получение групп параметров указанной команды.''' - pass - - -@router.get("/commands/{cid}/parameters", tags=["Commands management"], - dependencies=[Depends(right_checkers["read"])]) -async def get_command_parameters(cid: int) -> dict: - '''Обработчик запросов на получение параметров указанной команды.''' - pass - - -@router.post("/commands/{command_id}", tags=["Commands management"], - dependencies=[Depends(right_checkers["write"])]) -async def post_command(command_id: str) -> int: - if command_id not in data.commands: - # TODO добавить какую-то обработку ошибки. - pass - return +@router.get("/commands", response_model=GetCommandsResponse, tags=["Commands"]) +async def get_available_commands(request: Request, + gui: Optional[bool] = False): + response_data = commands.get_commands(get_base_url(request)) + return validate_response(response_data, GetCommandsResponse, + media_type="application/hal+json") + + +@router.get("/commands/{command}", + response_model=FoundCommandInfo, + tags=["Commands"]) +async def find_command_data(command: str, request: Request, + gui: Optional[bool] = False, + by_id: Optional[bool] = False): + base_url = get_base_url(request) + + if by_id: + command_data = commands.get_by_id(command, base_url) + if command_data is None: + raise get_command_not_found(command) + else: + command_data = commands.find_command(command, base_url) + if command_data is None: + raise get_cl_command_not_found(command) + + return validate_response(command_data, FoundCommandInfo, + media_type="application/hal+json") + + +@router.get("/commands/{command_id}/parameters", + response_model=GetCommandParametersResponse, + tags=["Commands"]) +async def get_command_parameters(command_id: str, request: Request): + parameters_data = commands.get_parameters(command_id, + get_base_url(request)) + if parameters_data is None: + raise get_command_not_found(command_id) + return parameters_data diff --git a/calculate/server/schemas/workers.py b/calculate/server/schemas/commands.py similarity index 100% rename from calculate/server/schemas/workers.py rename to calculate/server/schemas/commands.py diff --git a/calculate/server/schemas/requests.py b/calculate/server/schemas/requests.py new file mode 100644 index 0000000..1c3c0c6 --- /dev/null +++ b/calculate/server/schemas/requests.py @@ -0,0 +1,22 @@ +from typing import Any, List +from pydantic import BaseModel + + +# PATCH /configs/{WID}/parameters +class ParameterValue(BaseModel): + id: str + value: Any + + +class ModifyConfigRequest(BaseModel): + __root__: List[ParameterValue] + + +# PUT /configs/{WID}/parameters +class ResetConfigRequest(BaseModel): + __root__: List[ParameterValue] + + +# PATCH /executions/{WID}/input +class WriteToExecutionRequest(BaseModel): + data: Any diff --git a/calculate/server/schemas/responses.py b/calculate/server/schemas/responses.py new file mode 100644 index 0000000..54530dd --- /dev/null +++ b/calculate/server/schemas/responses.py @@ -0,0 +1,180 @@ +from typing import Any, List, Dict, Optional, Literal, Union +from pydantic import BaseModel, Field, HttpUrl + + +class NotTemplatedLinkData(BaseModel): + href: HttpUrl = "URI" + + class Config: + extra = "forbid" + + +class TemplatedLinkData(NotTemplatedLinkData): + templated: bool = False + + +LinkData = Union[NotTemplatedLinkData, TemplatedLinkData] + + +# GET / +class ServerInfo(BaseModel): + name: str = "Server name" + version: str = "Server version" + + +class GetRootResponse(BaseModel): + data: ServerInfo + links: Dict[str, LinkData] = Field(..., alias="_links", + description="Links to resources") + + +# GET /commands/{CID}/parameters +class ParameterInfo(BaseModel): + id: str + default: Any + + +class ParametersGroup(BaseModel): + group_id: str + parameters: List[ParameterInfo] + + +class GetCommandParametersResponse(BaseModel): + data: List[ParametersGroup] + links: Dict[str, LinkData] = Field(..., alias="_links") + + +# GET /commands/{CID} +class CommandData(BaseModel): + id: str + title: str + category: str + command: str + + +class CommandInfo(BaseModel): + data: CommandData + links: Dict[str, LinkData] = Field(..., alias="_links") + + +class FoundCommandInfo(CommandInfo): + embedded: Dict[str, + GetCommandParametersResponse] = Field(..., + alias="_embedded") + + +# GET /commands/ +class GetCommandsResponse(BaseModel): + __root__: Dict[str, CommandInfo] + + # class Config: + # @staticmethod + # def schema_extra(schema, model): + # example = {} + # for num in range(2): + # resp = ResponseStructure() + # resp.add_data(id=f"command_{num}", + # command=f"cl-command-{num}", + # title=f"Command {num}", + # category="cool-category" + # ) + # resp.add_link("parameters", + # f"/commands/command_{num}/parameters") + # resp.add_link("configure", f"/configs/command_{num}") + + # example[f"command_{num}"] = resp.get_dict() + # schema["example"] = example + + +# POST /configs/{command} +class CreateConfigResponse(BaseModel): + links: Dict[str, LinkData] = Field(..., alias="_links") + + +# PATCH /configs/{WID}/parameters +class ModifyConfigCorrectResponse(BaseModel): + links: Dict[str, LinkData] = Field(..., alias="_links") + + class Config: + extra = "forbid" + + +class ModifyConfigUncorrectResponse(ModifyConfigCorrectResponse): + data: Optional[Dict[str, str]] + links: Dict[str, LinkData] = Field(..., alias="_links") + + +ModifyConfigResponse = Union[ModifyConfigCorrectResponse, + ModifyConfigUncorrectResponse] + + +# PUT /configs/{WID}/parameters +class ResetConfigResponse(BaseModel): + data: Optional[Dict[str, str]] + links: Dict[str, LinkData] = Field(..., alias="_links") + + +# POST /executions/{WID} +class CreateExecutionResponse(BaseModel): + links: Dict[str, LinkData] = Field(..., alias="_links") + + +# DELETE /executions/{WID} +class OutputData(BaseModel): + type: Literal["output"] + logging: Union[int, None] + text: str + source: str + + +class OutputMessage(BaseModel): + data: OutputData + + +class StopExecutionsResponse(BaseModel): + __root__: List[OutputMessage] + + +# DELETE /configs/{WID} +class StopConfigResponse(BaseModel): + __root__: List[OutputMessage] + + +# GET /executions/{WID}/output +class InputRequestData(BaseModel): + type: Literal["input"] + text: str + source: str + + +class InputMessage(BaseModel): + data: InputRequestData + links: Dict[str, LinkData] = Field(..., alias="_links") + + +class GetExecutionOutputResponse(BaseModel): + __root__: List[Union[OutputMessage, InputMessage]] + + +# GET /workers/ +class WorkerData(BaseModel): + socket: str + command: str + pid: int + + +class WorkerInfo(BaseModel): + data: WorkerData + links: Dict[str, LinkData] = Field(..., alias="_links") + + +class GetWorkersResponse(BaseModel): + __root__: Dict[int, WorkerInfo] + +# DELETE /workers/{WID} + + +# Сообщения протокола взаимодействия сервера и воркеров. +# class WorkerMessageBase(BaseModel): +# state: str +# status: int diff --git a/calculate/server/server.py b/calculate/server/server.py index d47591a..784e2ca 100644 --- a/calculate/server/server.py +++ b/calculate/server/server.py @@ -1,15 +1,33 @@ from fastapi import FastAPI, Depends +from starlette.requests import Request from .server_data import ServerData -from .utils.dependencies import right_checkers -from .models.database import database +from .database.db import database + +# from .utils.dependencies import right_checkers, get_current_user +from .utils.dependencies import get_current_user +from .utils.responses import ( + ResponseStructure, + get_base_url, + validate_response, + ) +from .utils.users import check_user_rights + +from .schemas.responses import GetRootResponse +from .schemas.users import User + from .routers.users import router as users_router from .routers.commands import router as commands_router # TODO -# 1. Разобраться с описанием команд как ресурсов и со всем, что от них зависит. -# 2. Разобраться с объектами воркеров. И способом их функционирования. +# 1. Добавить список отклоняемых токенов и их обработку. +# 2. Добавить предварительный вариант генерации access_tokens. +# 3. Продумать, наконец, концепцию cid и wid, в ключе их применения при +# настройке команд и запуске воркеров. +# 4. Решить сколько команд может запускать пользователь единовременно. +# 5. Добавить способ указания принадлежности воркеров и команд пользователям. +# 6. Разобраться с местом раcположения базы данных. data = ServerData() @@ -26,30 +44,39 @@ async def shutdown(): await database.disconnect() -@app.get("/", tags=["Root"], - dependencies=[Depends(right_checkers["read"])]) -async def get_root() -> dict: - '''Обработчик корневых запросов.''' - return {'msg': 'root msg'} +@app.get("/", response_model=GetRootResponse, tags=["Home"]) +async def get_primary_commands(request: Request, + user: User = Depends(get_current_user)): + check_user_rights(user, "read") + + resp = ResponseStructure(get_base_url(request)) + resp.add_data(name="Calculate Server", version="0.1") + resp.add_link("commands", "/commands?gui={is_gui}", templated=True) + resp.add_link("find_command", "/commands/{console_command}?gui={is_gui}", + templated=True) + resp.add_link("workers", "/workers") + + return validate_response(resp.get_dict(), GetRootResponse, + media_type="application/hal+json") -@app.get("/workers/{wid}", tags=["Workers management"], - dependencies=[Depends(right_checkers["write"])]) -async def get_worker(wid: int): - '''Тестовый обработчик.''' - worker = data.get_worker_object(wid=wid) - worker.run() - print(f"worker: {type(worker)} object {worker}") +# @app.get("/workers/{wid}", tags=["Workers management"], +# dependencies=[Depends(right_checkers["write"])]) +# async def get_worker(wid: int): +# '''Тестовый обработчик.''' +# worker = data.get_worker_object(wid=wid) +# worker.run() +# print(f"worker: {type(worker)} object {worker}") - await worker.send({"text": "INFO"}) - worker_data = await worker.get() +# await worker.send({"text": "INFO"}) +# worker_data = await worker.get() - if worker_data['type'] == 'log': - data.log_message[data['level']](data['msg']) - return worker_data +# if worker_data['type'] == 'log': +# data.log_message[data['level']](data['msg']) +# return worker_data # Authentification and users management. app.include_router(users_router) -# Commands creation and management. +# Commands running and management. app.include_router(commands_router) diff --git a/calculate/server/server_data.py b/calculate/server/server_data.py index 9448162..307e6ab 100644 --- a/calculate/server/server_data.py +++ b/calculate/server/server_data.py @@ -1,20 +1,25 @@ import os import asyncio import importlib -from typing import Dict, Optional +from typing import Dict, Optional, List, Union from logging.config import dictConfig from logging import getLogger, Logger from ..variables.loader import Datavars from ..commands.commands import Command, CommandRunner -from .utils.workers import Worker +from .utils.responses import ResponseStructure +# from .utils.workers import Worker from .schemas.config import ConfigSchema +from calculate.utils.tools import Singleton + # Получаем конфигурацию сервера. TESTING = bool(os.environ.get("TESTING", False)) + + if not TESTING: from .config import config server_config = ConfigSchema(**config) @@ -23,7 +28,7 @@ else: server_config = ConfigSchema(**config) -class ServerData: +class ServerData(metaclass=Singleton): def __init__(self, config: ConfigSchema = server_config): self.event_loop = asyncio.get_event_loop() @@ -48,7 +53,8 @@ class ServerData: self.commands_runners: Dict[str, CommandRunner] = {} # Словарь WID и экземпляров процессов-воркеров, передаваемых воркерам. - self.workers: Dict[int, Worker] = {} + # TODO добавить менеджера воркеров. + # self.workers: Dict[int, Worker] = {} @property def datavars(self): @@ -87,18 +93,105 @@ class ServerData: def make_command(self, command_id: str, ) -> int: '''Метод для создания команды по ее описанию.''' - command_description = self.commands[command_id] - - def _get_worker_object(self, wid: Optional[int] = None) -> Worker: - '''Метод для получения воркера для команды.''' - if wid is not None: - worker = Worker(wid, self._event_loop, self._datavars) - self._workers[wid] = worker - elif not self._workers: - worker = Worker(0, self._event_loop, self._datavars) - self._workers[0] = worker + # command_description = self.commands[command_id] + + # def _get_worker_object(self, wid: Optional[int] = None) -> Worker: + # '''Метод для получения воркера для команды.''' + # if wid is not None: + # worker = Worker(wid, self._event_loop, self._datavars) + # self._workers[wid] = worker + # elif not self._workers: + # worker = Worker(0, self._event_loop, self._datavars) + # self._workers[0] = worker + # else: + # wid = max(self._workers.keys()) + 1 + # worker = Worker(wid, self._event_loop, self._datavars) + # self._workers[wid] = worker + # return worker + + +class Commands: + '''Предварительная реализация контейнера описаний команд.''' + def __init__(self, *commands: List[Command]): + self._by_id: dict = dict() + self._by_command: dict = dict() + for command in commands: + self._by_id[command.id] = command + self._by_command[command.command] = command + + def get_commands(self, base_url: str) -> Dict[str, dict]: + response = dict() + + for command in self._by_id.values(): + data = ResponseStructure(base_url) + data.add_data(id=command.id, + title=command.title, + category=command.category, + command=command.command) + data.add_link("self", f"/commands/{command.id}?by_id=true") + data.add_link("parameters", f"/commands/{command.id}/parameters") + data.add_link("configure", f"/configs/{command.id}") + response[command.id] = data.get_dict() + + return response + + def get_by_id(self, command_id: str, base_url: str) -> Dict[str, Dict]: + if command_id in self._by_id: + command = self._by_id[command_id] + data = ResponseStructure(base_url) + data.add_data(id=command.id, + title=command.title, + category=command.category, + command=command.command) + data.add_link("self", f"/commands/{command.id}?by_id=1") + data.add_link("parameters", f"/commands/{command.id}/parameters") + data.add_link("configure", f"/configs/{command.id}") + parameters_data = self._get_parameters_data(command.parameters, + command.id, base_url) + data.embed("parameters", parameters_data) + return data.get_dict() + + return None + + def find_command(self, console_command: str, + base_url: str) -> Union[dict, None]: + if console_command in self._by_command: + command = self._by_command[console_command] + + data = ResponseStructure(base_url) + data.add_data(id=command.id, + title=command.title, + category=command.category, + command=command.command) + data.add_link("self", f"/commands/{command.id}?by_id=1") + data.add_link("parameters", f"/commands/{command.id}/parameters") + data.add_link("configure", f"/configs/{command.id}") + parameters_data = self._get_parameters_data(command.parameters, + command.id, base_url) + data.embed("parameters", parameters_data) + + return data.get_dict() + else: + return None + + def get_parameters(self, command_id: str, base_url: str + ) -> Union[dict, None]: + if command_id in self._by_id: + command = self._by_id[command_id] + parameters_data = self._get_parameters_data(command.parameters, + command.id, base_url) + return parameters_data else: - wid = max(self._workers.keys()) + 1 - worker = Worker(wid, self._event_loop, self._datavars) - self._workers[wid] = worker - return worker + return None + + def get_command_object(self, command_id: str) -> Command: + if command_id in self._by_id: + return self._by_id[command_id] + return None + + def _get_parameters_data(self, parameters: list, command_id: str, + base_url: str) -> List[dict]: + data = ResponseStructure(base_url) + data.add_data(parameters) + data.add_link("self", f"/commands/{command_id}/parameters") + return data.get_dict() diff --git a/calculate/server/utils/auth.py b/calculate/server/utils/auth.py index a1b8ab9..69a8812 100644 --- a/calculate/server/utils/auth.py +++ b/calculate/server/utils/auth.py @@ -14,7 +14,7 @@ from .users import get_user_by_username from calculate.utils.files import Process -def make_secret_key(): +def make_secret_key() -> str: openssl_process = Process("/usr/bin/openssl", "rand", "-hex", "32") secret_key = openssl_process.read() return secret_key.strip() diff --git a/calculate/server/utils/daemon.py b/calculate/server/utils/daemon.py new file mode 100644 index 0000000..a4d6e3c --- /dev/null +++ b/calculate/server/utils/daemon.py @@ -0,0 +1,5 @@ +from multiprocessing.connection import Listener + + +def daemon(listener: Listener, wid: int, base_dir: str, command): + pass diff --git a/calculate/server/utils/responses.py b/calculate/server/utils/responses.py new file mode 100644 index 0000000..a711bf6 --- /dev/null +++ b/calculate/server/utils/responses.py @@ -0,0 +1,122 @@ +from fastapi import HTTPException, status +from fastapi.encoders import jsonable_encoder + +from starlette.responses import JSONResponse +from starlette.requests import Request + +from pydantic.main import ModelMetaclass +from pydantic import parse_obj_as + +from typing import Dict, Union, _GenericAlias, Optional + + +class ResponseStructure: + '''Класс структуры данных, для получения HATEOAS ответа на запросы + клиента.''' + def __init__(self, base_url: str): + self._base_url: str = base_url.strip("/") + + self._data: dict = dict() + self._links: Dict[str, Dict[str, str]] = dict() + self._embedded: Dict[str, dict] = dict() + + def get_dict(self) -> dict: + dictionary = dict() + if self._data: + dictionary["data"] = self._data + if self._links: + dictionary["_links"] = self._links + if self._embedded: + dictionary["_embedded"] = self._embedded + return dictionary + + def add_data(self, *args, **kwargs) -> "ResponseStructure": + if args: + if isinstance(self._data, dict): + if len(args) > 1: + self._data = list(args) + else: + self._data = args[0] + elif isinstance(self._data, list): + if len(args) > 1: + self._data.extend(args) + else: + self._data.append(args[0]) + else: + self._data = [self._data, *args] + elif kwargs: + if not isinstance(self._data, dict): + self._data = {} + self._data.update(kwargs) + return self + + def embed(self, resource: str, + content: Union[dict, list]) -> "ResponseStructure": + self._embedded[resource] = content + return self + + def add_link(self, action: str, uri: str, + templated: bool = False) -> "ResponseStructure": + uri = self._get_uri(uri) + + if action in self._links: + link_section = self._links[action] + if link_section["href"] != uri: + link_section["href"] = uri + else: + link_section = {"href": uri} + self._links[action] = link_section + + if templated: + print("TEMPLATED:", action) + link_section["templated"] = templated + + return self + + def _get_uri(self, uri: str) -> str: + if uri.startswith(self._base_url): + return uri + return f"{self._base_url}/{uri.strip('/')}" + + +def get_base_url(request: Request) -> str: + base_url = f"{request.base_url.scheme}://{request.base_url.netloc}" + return base_url + + +def get_command_not_found(command_id: str) -> HTTPException: + return HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=(f'Command with id "{command_id}"' + ' is not found.')) + + +def get_cl_command_not_found(console_command: str) -> HTTPException: + return HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=(f'Console command "{console_command}" is not' + ' found.')) + + +def get_configuration_not_found(wid: int) -> HTTPException: + return HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=f"Configuration with id={wid} is not found.") + + +def validate_response(data: Union[dict, list], + schema: Union[ModelMetaclass, _GenericAlias, type], + media_type: Optional[str] = None, + status_code: int = 200 + ) -> JSONResponse: + """Функция для валидации данных ответа сервера по указанной схеме с учетом + наличия псевдонимов полей и возможности того, что схема задана обычным + типом, типом из typing или BaseModel с __root__. Возвращает объект ответа. + """ + if isinstance(schema, ModelMetaclass): + if "__root__" in schema.__annotations__: + validated = schema(__root__=data) + else: + validated = schema(**data) + elif isinstance(schema, (_GenericAlias, type)): + validated = parse_obj_as(schema, data) + + return JSONResponse(content=jsonable_encoder(validated, by_alias=True), + media_type=media_type, status_code=status_code) diff --git a/calculate/server/utils/users.py b/calculate/server/utils/users.py index 44eedc9..5b9d28c 100644 --- a/calculate/server/utils/users.py +++ b/calculate/server/utils/users.py @@ -1,30 +1,31 @@ +from fastapi import HTTPException, status from sqlalchemy import func, and_ from typing import List -from ..models.database import database -from ..models.users import users_table, users_rights, rights_table +from ..database.db import database +from ..database.users import users_table as ut, users_rights, rights_table -from ..schemas.users import UserCreate +from ..schemas.users import UserCreate, User async def get_user_by_username(username: str): '''Метод для получения строки с данными пользователя из базы данных по username.''' - query = users_table.select().where(users_table.c.login == username) + query = ut.select().where(ut.c.login == username) user_data = await database.fetch_one(query) - query = (users_table. + query = (ut. join(users_rights). join(rights_table). select(). - where(and_(users_table.c.id == users_rights.c.user_id, - users_table.c.login == username, + where(and_(ut.c.id == users_rights.c.user_id, + ut.c.login == username, rights_table.c.id == users_rights.c.right_id)). - with_only_columns([users_table.c.id, - users_table.c.login, - users_table.c.password, + with_only_columns([ut.c.id, + ut.c.login, + ut.c.password, func.group_concat(rights_table.c.name, ' ').label("rights")]). - group_by(users_table.c.id)) + group_by(ut.c.id)) response = await database.fetch_one(query) user_data = dict(response) @@ -33,6 +34,16 @@ async def get_user_by_username(username: str): async def create_user(username: str, hashed_password: str, rights: List[str]): - user = UserCreate(login=username, - password=hashed_password, - rights=rights) + UserCreate(login=username, + password=hashed_password, + rights=rights) + + +def check_user_rights(user_data: User, *rights: List[str]) -> None: + user_rights = user_data.rights + for right in user_rights: + if right not in user_rights: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail='Not enough permissions', + headers={"WWW-Authenticate": "Bearer"} + ) diff --git a/calculate/server/utils/workers.py b/calculate/server/utils/workers.py index 98c99e3..c85e42f 100644 --- a/calculate/server/utils/workers.py +++ b/calculate/server/utils/workers.py @@ -1,12 +1,415 @@ import os +import sys import json -import socket +import struct +import signal +import asyncio import logging -from typing import Union -from calculate.variables.loader import Datavars -from multiprocessing import Queue, Process -from calculate.commands.commands import CommandRunner, Command -# from time import sleep +from multiprocessing import Process +from multiprocessing.connection import Listener, Connection +# import logging +# import time +from .daemon import daemon +# from commands import Command + +from typing import Dict, Optional, List, Union, Tuple, Literal + +from asyncio.unix_events import _UnixSelectorEventLoop +from asyncio import get_event_loop + + +LOG_LEVELS = {logging.ERROR: "ERROR", + logging.INFO: "INFO", + logging.WARNING: "WARNING", + logging.DEBUG: "DEBUG", + logging.CRITICAL: "CRITICAL", + } + + +class WorkerException(Exception): + pass + + +class UnexpectedWorkerFinish(WorkerException): + def __init__(self, reason: List[str], output: Optional[List[str]] = []): + """Исключение кидаемое в случае, если воркер неожиданно умер.""" + self.reason: List[str] = reason + self.output: List[dict] = output + + +class UnexpectedWorkerState(WorkerException): + """Исключение кидаемое в случае, если состояние воркера не соответствует + ожидаемому.""" + def __init__(self, state: str, expected: str): + self.state: str = state + self.expected: str = expected + + +class DaemonIsDead(WorkerException): + """Исключение кидаемое в случае, если демон-воркер неожиданно оказался + мертв.""" + def __init__(self, info: str, output: Optional[List[str]] = []): + self.info: str = info + self.output: List[dict] = output + + +class IncorrectConfiguration(WorkerException): + """Исключение кидаемое при попытке запустить выполнение конфигурации, в + которой есть ошибки.""" + def __init__(self, errors: Dict[str, str]): + self._errors = errors + + +class WorkersManager: + def __init__(self, loop: Optional[_UnixSelectorEventLoop] = None): + if loop is None: + self._loop = get_event_loop() + else: + self._loop = loop + + self._configs: Dict[int, WorkerProtocol] = {} + self._execs: Dict[int, WorkerProtocol] = {} + self._wids: List[int] = [] + + async def make_worker(self, command) -> Tuple[Union[int, None], + Union[str, None]]: + """Метод для создания воркеров.""" + wid = self._get_wid() + try: + # Создаем воркера и получаем интерфейс для взаимодействия с ним. + worker_api = await self._create_worker(wid, command) + self._configs[wid] = worker_api + except Exception as error: + if wid in self._configs: + self._configs.pop(wid) + return None, str(error) + return wid, None + + async def configure_worker(self, wid: int, parameters: List[dict], + reset: bool = False) -> Union[dict, None]: + """Метод для модификации или сброса параметров конфигурации указанными + значениями. + """ + if not parameters: + return {} + if wid not in self._configs: + return None + + worker_api: WorkerProtocol = self._configs[wid] + try: + result = await worker_api.configure(parameters) + except (UnexpectedWorkerFinish, DaemonIsDead): + worker_api.kill() + self._configs.pop(wid) + raise + + return result + + async def cancel_worker_configuration(self, wid: int): + """Метод для завершения конфигурации воркера и завершения его работы. + """ + if wid not in self._configs: + return None + + worker_api: WorkerProtocol = self._configs[wid] + output = await worker_api.cancel_configuration() + worker_api.delete_socket() + self._configs.pop(wid) + + return output + + def clean(self): + """Метод для убийства всех работающих на данный момент демонов.""" + # TODO доработать его логику так, чтобы при перезапуске сервера этот + # метод никого не убивал. + for wid, worker in {**self._configs, **self._execs}.items(): + result = False + try: + result = worker.kill() + finally: + if result: + print(f"[*] Worker {wid} is killed.") + else: + print(f"[x] Worker {wid} is not killed.") + continue + + def get_config(self, wid: int): + if wid in self._configs: + return self._configs[wid] + return None + + def get_execution(self, wid: int): + if wid in self._execs: + return self._execs[wid] + return None + + def run_execution(self, wid: int) -> Tuple[int, Union[dict, None]]: + '''Метод для запуска исполнения команды, используя указанную + конфигурацию. + Вывод: + 0, None -- исполнение успешно запущено; + 1, None -- конфигурация с данным wid не найдена; + 2, errors -- конфигурация с данным wid содержит ошибки; + 3, None -- исполнение с данным wid уже существует; + ''' + if wid not in self._configs: + return 1, None + if self._configs[wid].status > 0: + # Берем ошибки, как-то. + errors = {} + return 2, errors + if wid in self._execs: + return 3, None + + # worker = self._configs.pop(wid) + return 0, None + + async def _create_worker(self, wid: int, command) -> "WorkerProtocol": + # Путь к временному файлу, в котором будет лежать pid демона-рабочего. + pid_file_path: str = os.path.join(os.getcwd(), f"pid_file_{wid}") + socket_path: str = os.path.join(os.getcwd(), + f"worker_{wid}.sock") + if os.path.exists(socket_path): + os.remove(socket_path) + + process_runner = Process(target=daemonize_worker, + args=(socket_path, wid, pid_file_path, + command) + ) + process_runner.start() + process_runner.join() + + daemon_pid = await self._get_daemon_pid(pid_file_path) + self._delete_pid_file(pid_file_path) + + transport, worker_interface = await self._loop.create_unix_connection( + lambda: WorkerProtocol(daemon_pid, + socket_path), + socket_path) + return worker_interface + + def _get_wid(self): + if not self._wids: + worker_id = 0 + else: + worker_id = self._wids[-1] + 1 + self._wids.append(worker_id) + return worker_id + + async def _get_daemon_pid(self, pid_file_path: str) -> int: + while True: + if not os.path.exists(pid_file_path): + # Если файла нет -- уступаем работу другим воркерам. + await asyncio.sleep(0) + with open(pid_file_path, "r") as pid_file: + daemon_pid = int(pid_file.readline()) + return daemon_pid + + def _delete_pid_file(self, pid_file_path: str) -> None: + try: + os.remove(pid_file_path) + except OSError: + pass + + def __repr__(self): + configs = ["Configurations:"] + configs.extend([f"wid={wid}" for wid in self._configs.keys()]) + configs_repr = "\n\t".join(configs) + + execs = ["Executions:"] + execs.extend([f"wid={wid}" for wid in self._execs.keys()]) + execs_repr = "\n\t".join(execs) + + return f"{configs_repr}\n{execs_repr}" + + +def daemonize_worker(socket_path: str, wid: int, pid_file: str, + command) -> None: + '''Функция запускаемая в отдельном процессе и порождающая демона при помощи + магии двойного форка.''' + base_dir = os.getcwd() + + new_fork() + + os.chdir("/") + os.setsid() + os.umask(0) + + new_fork() + + # Создаем файл, с помощью которого будем передавать pid демона-рабочего. + daemon_pid = str(os.getpid()) + with open(pid_file, "w") as pid_file: + pid_file.write(daemon_pid) + + with Listener(socket_path) as listener: + daemon(listener, wid, base_dir, command) + + +def new_fork() -> None: + '''Функция для создания форка текущего процесса.''' + try: + middle_pid = os.fork() + if middle_pid > 0: + sys.exit(0) + except OSError as error: + print(f"FORK ERROR: {error.errno} - {str(error)}") + sys.exit(1) + + +class WorkerProtocol(asyncio.Protocol): + """Класс протокола управления воркером.""" + def __init__(self, pid: int, socket_path: str): + self.loop = asyncio.get_running_loop() + + self._pid: int = pid + self._socket_path: str = socket_path + + # Объект Event, предназначенный для остановки записи при переполнении + # буффера записи в трaнспорте. + self._can_write = asyncio.Event() + self._can_write.clear() + + self._msg_queue = asyncio.Queue() + + # Контекст чтения данных воркера. + self._buffer = bytearray() + self._msg_size: int = 0 + + self._connection_closed: bool = True + + async def configure(self, parameters: List[dict], + reset: bool = False) -> list: + """Метод для конфигурации воркера указанными значениями параметров. + Возвращает список ошибок, если таковые есть, или пустой список, если + никаких ошибок нет. + """ + message = self._make_worker_message("config", int(reset), parameters) + await self._write(message) + response = await self._read() + response = json.loads(response) + + return response["data"] + + async def cancel_configuration(self) -> List[dict]: + """Метод для остановки конфигурации воркера и его работы в целом.""" + stop_message: bytes = self._make_worker_message("finish", 1, None) + await self._write(stop_message) + response = await self._read() + response = json.loads(response) + + return response["data"] + + def connection_made(self, transport: asyncio.Transport) -> None: + self.transport = transport + self._can_write.set() + self._connection_closed: bool = False + print("Connection made.") + + def connection_lost(self, exc: BaseException) -> None: + if exc: + print("Connection lost. Error:", str(exc)) + else: + print("Connection lost.") + self._connection_closed: bool = True + + def data_received(self, data: bytes) -> None: + self._buffer.extend(data) + + while True: + if not self._msg_size: + if len(self._buffer) < 4: + return + size_length = struct.calcsize(">i") + self._msg_size, = struct.unpack(">i", + self._buffer[:size_length]) + self._buffer = self._buffer[size_length:] + + if len(self._buffer) >= self._msg_size: + data = self._buffer[:self._msg_size] + self._buffer = self._buffer[self._msg_size:] + self._msg_size = 0 + + self._msg_queue.put_nowait(data.decode()) + else: + break + + def pause_writing(self) -> None: + self._can_write.clear() + + def resume_writing(self) -> None: + self._can_write.set() + + async def _read(self) -> str: + if self._connection_closed and self._msg_queue.empty(): + return None + return await self._msg_queue.get() + + async def read_all(self) -> List[str]: + if self._msg_queue.empty(): + if self._connection_closed: + return None + else: + return await self._msg_queue.get() + output = [] + while not self._msg_queue.empty(): + output.append(self._msg_queue.get_nowait()) + return output + + async def _write(self, data: Union[str, bytes]) -> None: + await self._can_write.wait() + if isinstance(data, str): + data = data.encode() + self.transport.write(struct.pack(">i", len(data))) + self.transport.write(data) + + def _make_worker_message( + self, + state: Literal["config", "finish", "exec", "input"], + status: int, data: Union[list, dict]) -> dict: + return json.dumps({"state": state, + "status": status, + "data": data}).encode() + + def delete_socket(self) -> None: + """Метод для удаления сокета.""" + try: + os.remove(self._socket_path) + self.transport.close() + self._connection_closed = True + except OSError: + pass + + def kill(self) -> bool: + """Метод для убийства демона.""" + print("GONNA KILLA DAEMON:", self._pid) + if self.is_daemon_alive(): + try: + os.kill(self._pid, signal.SIGKILL) + except OSError: + return False + else: + print("ALREADY DEAD") + return True + + def stop(self) -> bool: + print("STOP DAEMON:", self._daemon_pid) + if self.is_daemon_alive(): + try: + # Для остановки демона посылаем в него KeyboardInterrupt. + os.kill(self._pid, signal.SIGINT) + except OSError as error: + print(f"ERROR: Failed stopping daemon PID={self._pid}. " + f"Reason: {str(error)}") + return False + return True + + def is_daemon_alive(self) -> bool: + try: + os.kill(self._pid, 0) + return True + except OSError: + return False class WorkerIOError(KeyboardInterrupt): @@ -15,24 +418,24 @@ class WorkerIOError(KeyboardInterrupt): class IOModule: '''Класс модуля ввода/вывода для воркеров.''' - def __init__(self, socket_path: str): - self._sock = socket.socket(socket.AF_UNIX, - socket.SOCK_STREAM) - self._sock.bind(socket_path) - self._sock.listen() - self._connection = None + def __init__(self, listener: Listener, command_name: str): + self._listener: Listener = listener + self._connection: Union[Connection, None] = None - def input(self, msg: str) -> str: - '''Метод через который возможен ввод данных в скрипт.''' - input_request = json.dumps({"type": "input", - "msg": msg}) + '\0' - answer = None - while answer is None: - if not self._check_connection(self._connection): - self._connection = self._make_connection() - self._connection.sendall(input_request.encode()) - answer = self._get() - return answer['data'] + self._command: str = command_name + self._script: str = "" + + @property + def command(self) -> str: + return self._command + + @property + def script(self) -> str: + return self._script + + @script.setter + def script(self, script: str) -> None: + self._script = script def set_debug(self, msg: str) -> None: self.output(msg, level=logging.DEBUG) @@ -49,142 +452,57 @@ class IOModule: def set_critical(self, msg: str) -> None: self.output(msg, level=logging.CRITICAL) - def output(self, msg: str, level: int = logging.INFO) -> None: + def output(self, text: str, level: int = logging.INFO) -> None: '''Метод для отправки серверу вывода с указанием уровня.''' - if not self._check_connection(self._connection): - self._connection = self._make_connection() - output_request = json.dumps({"type": "output", - "level": level, - "msg": msg}) + '\0' - self._connection.sendall(output_request.encode()) + output_request = {"state": "exec", + "status": 0, + "data": { + "type": "message", + "logging": level, + "text": text, + "source": self._script + } + } + self.send(output_request) + + def input(self, msg: str) -> str: + '''Метод через который возможен ввод данных в скрипт.''' + input_request = {"state": "input", + "status": 0, + "data": {"text": msg, + "source": f"{self.command}:{self.script}"}} + + answer = None + while answer is None: + self.send(input_request) + answer = self.receive() + + return answer['data'] def send(self, data: dict) -> None: '''Метод для отправки данных серверу.''' - if not self._check_connection(self._connection): - self._connection = self._make_connection() - data = json.dumps(data) + '\0' - self._connection.sendall(data.encode()) + if self._connection is None: + raise WorkerIOError("No one is connected now.") + data = json.dumps(data).encode() + self._connection.send_bytes(data) def receive(self) -> dict: '''Метод для получения данных от сервера.''' - data = None - while data is None: - if not self._check_connection(self._connection): - self._connection = self._make_connection() - data = self._get() - return data - - def _get(self) -> Union[None, dict]: - '''Метод для считывания данных, возможно, поделенных на кадры.''' - try: - data = b'' - while True: - chunk = self._connection.recv(1024) - if not chunk: - return None - - data += chunk - if not data.endswith(b'\0'): - if b'\0' in data: - # Если после символа конца сообщения есть еще какие-то - # данные -- считаем их наличие ошибкой. - raise WorkerIOError("Unexpected message.") - continue - return json.loads(data[:-1].decode()) - except ConnectionResetError: - return None - - def _make_connection(self) -> socket.socket: - '''Метод для создания подключения.''' - connection, parent_address = self._sock.accept() - return connection + if self._connection is None: + raise WorkerIOError("No one is connected now.") + data: bytes = self._connection.recv_bytes() + return json.loads(data.decode()) - def _check_connection(self, connection: socket.socket) -> bool: - '''Метод для проверки соединения путем отправки на сокет пустого - сообщения.''' - if connection is None: - return False + def __enter__(self): + self._connection = self._listener.accept() + return self - try: - connection.sendall(b'') - return True - except BrokenPipeError: - return False - - def __del__(self) -> None: - if self._connection is not None: + def __exit__(self, exc_type, exc_value, exc_traceback): + if exc_type: + print("exc_type =", exc_type) + print("exc_value =", exc_value) + print("exc_traceback =", exc_traceback) + if exc_type is KeyboardInterrupt: + print("correctly") + if not self._connection.closed: self._connection.close() - self._sock.close() - - -class Worker: - def __init__(self, wid: int, command: Command, datavars: Datavars): - self._wid: int = wid - self._datavars: Datavars = datavars - - self._socket_path: str = f"./worker_{self._wid}.sock" - if os.path.exists(self._socket_path): - os.remove(self._socket_path) - self._input_queue: list = [] - - def run(self): - io = IOModule(self._socket_path) - title = io.receive() - print(title['msg']) - while True: - msg = "What is your desire?" - print(">", msg) - answer = io.input(msg) - print(f'> {answer}') - if answer == "stop": - break - elif answer == "output": - msg = "What kind of output you wanna see?" - print(">", msg) - answer = io.input(msg) - io.set_info(answer) - else: - msg = "I am sorry, but I could not help you(" - print(">", msg) - io.output(msg) - print('STOPPED') - - def initialize(self, io: IOModule): - pass - - def run_command(self): - pass - - -class DeprecatedWorker: - def __init__(self, wid, loop, sockets_path: str = 'calculate/server/'): - self._wid = wid - self._event_loop = loop - - # Создаем сокет для взаимодействия воркера и сервера. - socket_path = os.path.join(sockets_path, f'worker_{self._wid}') - if os.path.exists(socket_path): - os.remove(socket_path) - self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) - self._socket.bind(socket_path) - - @staticmethod - def _get_output(output_queue: Queue) -> dict: - return output_queue.get() - - @staticmethod - def _main_loop(command, in_queue: Queue, out_queue: Queue): - data = in_queue.get() - print('\nworker for command:', command) - output = {"type": "log", - "level": "INFO", - "msg": f"recieved message {data['text']}"} - out_queue.put(output) - - def run(self, command): - '''Метод для запуска процесса воркера с заданным ''' - worker_process = Process(target=self._main_loop, - args=(command, - self._in_queue, - self._output_queue)) - worker_process.start() diff --git a/calculate/templates/format/backgrounds_format.py b/calculate/templates/format/backgrounds_format.py index a4ce64f..9bd3354 100644 --- a/calculate/templates/format/backgrounds_format.py +++ b/calculate/templates/format/backgrounds_format.py @@ -6,7 +6,7 @@ from ...utils.images import ImageMagick, ImageMagickError from ...variables.loader import Datavars from .base_format import Format, FormatError -from typing import Union, List, Tuple, NoReturn +from typing import Union, List, Tuple import hashlib import re import os @@ -296,7 +296,7 @@ class BackgroundsFormat(Format): return md5_object.hexdigest() def _create_md5sum_file(self, target_path: str, action_md5sum: str - ) -> NoReturn: + ) -> None: """Метод для создания файла с md5-суммой действия, выполненного данным шаблоном.""" if not self._empty_name: diff --git a/calculate/templates/format/base_format.py b/calculate/templates/format/base_format.py index ba249cc..98eb443 100644 --- a/calculate/templates/format/base_format.py +++ b/calculate/templates/format/base_format.py @@ -23,6 +23,8 @@ class Format: CALCULATE_VERSION: Union[str, None] = None SHEBANG_PATTERN: str = r"^(?P#!\s*[\w\d\/]+\n)" + comment_symbol = None + def __init__(self, processing_methods: List[Callable]): self._processing_methods: List[Callable] = processing_methods self._document_dictionary: OrderedDict = OrderedDict() diff --git a/calculate/templates/format/bind_format.py b/calculate/templates/format/bind_format.py index 6bda7c6..9656173 100644 --- a/calculate/templates/format/bind_format.py +++ b/calculate/templates/format/bind_format.py @@ -1,13 +1,27 @@ # vim: fileencoding=utf-8 # -# ToDo: добавить проверку того, полностью ли парсился документ. Если отпарсился +# TODO: добавить проверку того, полностью ли парсился документ. Если отпарсился # не весь файл -- выдаем ошибку. # from .base_format import Format from collections import OrderedDict -from pyparsing import originalTextFor, OneOrMore, Word, alphanums, Literal,\ - ZeroOrMore, Forward, Optional, Group, restOfLine,\ - cppStyleComment, Keyword, printables, nums, SkipTo +from pyparsing import ( + originalTextFor, + OneOrMore, + Word, + alphanums, + Literal, + ZeroOrMore, + Forward, + Optional, + Group, + restOfLine, + cppStyleComment, + Keyword, + printables, + nums, + SkipTo + ) class BINDFormat(Format): diff --git a/calculate/templates/format/dovecot_format.py b/calculate/templates/format/dovecot_format.py index 8ae6b4b..a133697 100644 --- a/calculate/templates/format/dovecot_format.py +++ b/calculate/templates/format/dovecot_format.py @@ -1,6 +1,6 @@ # vim: fileencoding=utf-8 # -# ToDo: написать счетчик скобок для финальной оценки корректности +# TODO: написать счетчик скобок для финальной оценки корректности # документа. # from .base_format import Format diff --git a/calculate/templates/format/kde_format.py b/calculate/templates/format/kde_format.py index 2b3eda8..3b6dd5c 100644 --- a/calculate/templates/format/kde_format.py +++ b/calculate/templates/format/kde_format.py @@ -1,11 +1,20 @@ # vim: fileencoding=utf-8 # from .base_format import Format -from ..template_engine import ParametersContainer from collections import OrderedDict -from pyparsing import originalTextFor, Literal, ZeroOrMore, Word, printables,\ - OneOrMore, alphanums, ParseException, restOfLine,\ - Group, Optional, Regex +from pyparsing import ( + originalTextFor, + Literal, + ZeroOrMore, + Word, + printables, + OneOrMore, + alphanums, + ParseException, + restOfLine, + Group, + Optional + ) class KDEFormat(Format): diff --git a/calculate/templates/format/kernel_format.py b/calculate/templates/format/kernel_format.py index b1987ea..26d0d52 100644 --- a/calculate/templates/format/kernel_format.py +++ b/calculate/templates/format/kernel_format.py @@ -1,11 +1,18 @@ # vim: fileencoding=utf-8 # from .base_format import Format -from ..template_engine import ParametersContainer from collections import OrderedDict -from pyparsing import Word, Literal, alphanums, originalTextFor,\ - OneOrMore, ParseException, restOfLine, Group, Optional,\ - Regex +from pyparsing import ( + Word, + Literal, + alphanums, + originalTextFor, + ParseException, + restOfLine, + Group, + Optional, + Regex + ) class KernelFormat(Format): diff --git a/calculate/templates/format/patch_format.py b/calculate/templates/format/patch_format.py index 2289039..eb29f1c 100644 --- a/calculate/templates/format/patch_format.py +++ b/calculate/templates/format/patch_format.py @@ -2,7 +2,6 @@ # from .base_format import Format, FormatError from calculate.utils.files import Process -from typing import NoReturn from os import path @@ -25,7 +24,7 @@ class PatchFormat(Format): # Предупреждения. self._warnings: list = [] - def _init_command(self) -> NoReturn: + def _init_command(self) -> None: self._patch_cmd = "/usr/bin/patch" if not path.exists(self._patch_cmd): self._patch_cmd = None diff --git a/calculate/templates/format/procmail_format.py b/calculate/templates/format/procmail_format.py index 2ee20b7..2ff05fb 100644 --- a/calculate/templates/format/procmail_format.py +++ b/calculate/templates/format/procmail_format.py @@ -2,9 +2,19 @@ # from .base_format import Format from collections import OrderedDict -from pyparsing import Word, Literal, alphanums, printables, originalTextFor,\ - OneOrMore, ParseException, restOfLine, Group, Optional,\ - Regex +from pyparsing import ( + Word, + Literal, + alphanums, + printables, + originalTextFor, + OneOrMore, + ParseException, + restOfLine, + Group, + Optional, + Regex + ) class ProcmailFormat(Format): diff --git a/calculate/templates/format/samba_format.py b/calculate/templates/format/samba_format.py index a3fc7ca..eb57a2a 100644 --- a/calculate/templates/format/samba_format.py +++ b/calculate/templates/format/samba_format.py @@ -1,7 +1,6 @@ # vim: fileencoding=utf-8 # from .base_format import Format -from ..template_engine import ParametersContainer from collections import OrderedDict from pyparsing import originalTextFor, Literal, Word, printables, OneOrMore,\ alphanums, ParseException, Optional, Group, restOfLine @@ -26,7 +25,6 @@ class SambaFormat(Format): join_before=False, add_header=False, already_changed=False, - parameters=ParametersContainer(), **kwargs): processing_methods = [self._parse_comment_line, self._parse_section_line, diff --git a/calculate/templates/template_engine.py b/calculate/templates/template_engine.py index f3edb93..158b496 100644 --- a/calculate/templates/template_engine.py +++ b/calculate/templates/template_engine.py @@ -4,17 +4,19 @@ from jinja2.ext import Extension from jinja2.lexer import Token from jinja2.parser import Parser from jinja2 import ( + nodes, + Template, Environment, + contextfunction, FileSystemLoader, TemplateSyntaxError, - nodes, - contextfunction, - Template, ) from jinja2.utils import missing from jinja2.runtime import Context, Undefined + from collections.abc import MutableMapping from collections import OrderedDict + from importlib import import_module from pprint import pprint import copy @@ -22,37 +24,38 @@ import re import os import stat from typing import ( + Optional, Union, - Any, List, + Any, + Dict, Tuple, - NoReturn, - Optional, Iterator, ) + from ..utils.package import ( - PackageAtomName, PackageAtomParser, PackageAtomError, - Package, + PackageAtomName, NOTEXIST, - Version + Package, + Version, ) from ..utils.files import ( - join_paths, check_directory_link, check_command, - FilesError + join_paths, + FilesError, ) from calculate.variables.datavars import ( VariableNotFoundError, - HashType, NamespaceNode, VariableNode, - IniType, IntegerType, FloatType, - ListType + ListType, + HashType, + IniType, ) from calculate.utils.fs import readFile from calculate.variables.loader import Datavars @@ -116,10 +119,10 @@ class Variables(MutableMapping): def __getitem__(self, name: str) -> Any: return self.__attrs[name] - def __setitem__(self, name: str, value: Any) -> NoReturn: + def __setitem__(self, name: str, value: Any) -> None: self.__attrs[name] = value - def __delitem__(self, name: str) -> NoReturn: + def __delitem__(self, name: str) -> None: del self.__attrs[name] def __iter__(self) -> Iterator: @@ -254,7 +257,7 @@ class ParametersProcessor: def set_parameters_container(self, parameters_container: "ParametersContainer" - ) -> NoReturn: + ) -> None: '''Метод для установки текущего контейнера параметров.''' self._parameters_container = parameters_container self._added_parameters = set() @@ -264,7 +267,7 @@ class ParametersProcessor: return self._for_package @for_package.setter - def for_package(self, package: Package) -> NoReturn: + def for_package(self, package: Package) -> None: self._for_package = package def __getattr__(self, parameter_name: str) -> Any: @@ -278,7 +281,7 @@ class ParametersProcessor: def check_template_parameter(self, parameter_name: str, parameter_value: Any, - template_type: int, lineno: int) -> NoReturn: + template_type: int, lineno: int) -> None: '''Метод, проверяющий указанный параметр.''' self.lineno = lineno self.template_type = template_type @@ -313,7 +316,7 @@ class ParametersProcessor: self._parameters_container.set_parameter({parameter_name: checked_value}) - def check_postparse_parameters(self) -> NoReturn: + def check_postparse_parameters(self) -> None: '''Метод, запускающий проверку параметров после их разбора.''' for parameter, parameter_checker in\ self.postparse_checkers_list.items(): @@ -326,7 +329,7 @@ class ParametersProcessor: result) def check_template_parameters(self, parameters: dict, - template_type: int, lineno: int) -> NoReturn: + template_type: int, lineno: int) -> None: '''Метод, запускающий проверку указанных параметров.''' self.template_type = template_type self.lineno = lineno @@ -638,7 +641,7 @@ class ParametersProcessor: return parameter_value # Методы для проверки параметров после разбора всего шаблона. - def check_postparse_append(self, parameter_value: str) -> NoReturn: + def check_postparse_append(self, parameter_value: str) -> None: if parameter_value == 'link': if 'source' not in self._parameters_container: raise IncorrectParameter("append = 'link' without source " @@ -652,7 +655,7 @@ class ParametersProcessor: raise IncorrectParameter("'append' parameter is not 'compatible' " "with the 'exec' parameter") - def check_postparse_run(self, parameter_value: str) -> NoReturn: + def check_postparse_run(self, parameter_value: str) -> None: if self._parameters_container.append: raise IncorrectParameter("'run' parameter is not 'compatible' " "with the 'append' parameter") @@ -661,7 +664,7 @@ class ParametersProcessor: raise IncorrectParameter("'run' parameter is not 'compatible' " "with the 'exec' parameter") - def check_postparse_exec(self, parameter_value: str) -> NoReturn: + def check_postparse_exec(self, parameter_value: str) -> None: if self._parameters_container.append: raise IncorrectParameter("'exec' parameter is not 'compatible' " "with the 'append' parameter") @@ -672,7 +675,7 @@ class ParametersProcessor: def check_postparse_source(self, parameter_value: Union[str, Tuple[bool, str]] - ) -> NoReturn: + ) -> None: # Если файл по пути source не существует, но присутствует параметр # mirror -- пропускаем шаблон для того, чтобы целевой файл мог быть # удален в исполнительном модуле. @@ -692,12 +695,12 @@ class ParametersProcessor: "append = 'link' for directory template") ) - def check_postparse_autoupdate(self, parameter_value: bool) -> NoReturn: + def check_postparse_autoupdate(self, parameter_value: bool) -> None: if self._parameters_container.unbound: raise IncorrectParameter("'unbound' parameter is incompatible" " with 'autoupdate' parameter") - def check_postparse_handler(self, parameter_value: bool) -> NoReturn: + def check_postparse_handler(self, parameter_value: bool) -> None: if self._parameters_container.merge: raise IncorrectParameter("'merge' parameter is not available" " in handler templates") @@ -706,7 +709,7 @@ class ParametersProcessor: raise IncorrectParameter("'package' parameter is not available" " in handler templates") - def check_postparse_package(self, parameter_value: str) -> NoReturn: + def check_postparse_package(self, parameter_value: str) -> None: groups = [] package_atom = PackageAtomParser.parse_atom_name(parameter_value) @@ -760,19 +763,19 @@ class ParametersProcessor: return True return False - def check_postparse_convert(self, parameter_value: str) -> NoReturn: + def check_postparse_convert(self, parameter_value: str) -> None: template_format = self._parameters_container.format if not template_format or template_format != "backgrounds": raise IncorrectParameter("'convert' parameter available for" " 'backgrounds' format only.") - def check_postparse_stretch(self, parameter_value: str) -> NoReturn: + def check_postparse_stretch(self, parameter_value: str) -> None: template_format = self._parameters_container.format if not template_format or template_format != "backgrounds": raise IncorrectParameter("'stretch' parameter available for" " 'backgrounds' format only.") - def check_postparse_execsql(self, parameter_value: str) -> NoReturn: + def check_postparse_execsql(self, parameter_value: str) -> None: template_format = self._parameters_container.format if not template_format or template_format != "sqlite": raise IncorrectParameter("'execsql' parameter available for" @@ -876,7 +879,7 @@ class ParametersProcessor: format(group_file_path)) @classmethod - def _inspect_formats_package(cls) -> NoReturn: + def _inspect_formats_package(cls) -> None: '''Метод для определения множества доступных форматов и предоставляемых ими параметров.''' if cls.format_is_inspected: @@ -901,6 +904,7 @@ class ParametersProcessor: module = import_module('calculate.templates.format.{}'. format(module_name)) for obj in dir(module): + # TODO изменить способ выборки классов форматов. if obj.endswith('Format') and obj != 'BaseFormat': format_class = getattr(module, obj, False) @@ -954,6 +958,11 @@ class CalculateContext(Context): сохранять их.''' _env_set = set() + def __init__(self, environment: Environment, + parent: Union[Context, Dict[str, Any]], + name: str, blocks: Dict[str, Any], **kwargs): + super().__init__(environment, parent, name, blocks, **kwargs) + def resolve(self, key: str) -> Any: if self._legacy_resolve_mode: rv = resolve_or_missing(self, key, @@ -987,19 +996,19 @@ class ParametersContainer(MutableMapping): else: self.__inheritable: dict = {} - def set_parameter(self, item_to_add: dict) -> NoReturn: + def set_parameter(self, item_to_add: dict) -> None: self.__parameters.update(item_to_add) - def set_inheritable(self, item_to_add: dict) -> NoReturn: + def set_inheritable(self, item_to_add: dict) -> None: self.__inheritable.update(item_to_add) def get_inheritables(self) -> "ParametersContainer": return ParametersContainer(copy.deepcopy(self.__inheritable)) - def remove_not_inheritable(self) -> NoReturn: + def remove_not_inheritable(self) -> None: self.__parameters.clear() - def print_parameters_for_debug(self) -> NoReturn: + def print_parameters_for_debug(self) -> None: print('Parameters:') pprint(self.__parameters) @@ -1010,19 +1019,19 @@ class ParametersContainer(MutableMapping): return (parameter_name not in self.__parameters and parameter_name in self.__inheritable) - def remove_parameter(self, parameter_name: str) -> NoReturn: + def remove_parameter(self, parameter_name: str) -> None: if parameter_name in self.__parameters: self.__parameters.pop(parameter_name) elif parameter_name in self.__inheritable: self.__inheritable.pop(parameter_name) - def change_parameter(self, parameter: str, value: Any) -> NoReturn: + def change_parameter(self, parameter: str, value: Any) -> None: if parameter in self.__parameters: self.__parameters.update({parameter: value}) elif parameter in self.__inheritable: self.__inheritable.update({parameter: value}) - def _clear_container(self) -> NoReturn: + def _clear_container(self) -> None: self.__parameters.clear() self.__inheritable.clear() @@ -1047,10 +1056,10 @@ class ParametersContainer(MutableMapping): else: return False - def __setitem__(self, name: str, value: Any) -> NoReturn: + def __setitem__(self, name: str, value: Any) -> None: self.__parameters[name] = value - def __delitem__(self, name: str) -> NoReturn: + def __delitem__(self, name: str) -> None: if name in self.__parameters: del self.__parameters[name] @@ -1092,9 +1101,9 @@ class CalculateExtension(Extension): self.environment: Environment = environment self.package_atom_parser = PackageAtomParser(chroot_path=chroot_path) - self.environment.globals.update({'pkg': self.pkg}) - self.environment.globals.update({'grep': self.grep}) - self.environment.globals.update({'exists': self.exists}) + self.environment.globals.update({'pkg': self.pkg, + 'grep': self.grep, + 'exists': self.exists}) self._datavars = datavars_module self.parameters_processor = parameters_processor @@ -1104,7 +1113,7 @@ class CalculateExtension(Extension): # того, чтобы проверять единственность тега calculate. self.calculate_parsed: bool = False - self.tags = {'calculate', 'save', 'set_var'} + self.tags = {'calculate', 'save'} self.CONDITION_TOKENS_TYPES = {'eq', 'ne', 'lt', 'gt', 'lteq', 'gteq'} self.CONDITION_NAME_TOKENS = {'not'} self.LITERAL_TOKENS_TYPES = {'string', 'integer', 'float'} @@ -1121,6 +1130,8 @@ class CalculateExtension(Extension): def __call__(self, env: Environment) -> "CalculateExtension": # Необходимо для обеспечения возможности передать готовый объект # расширения, а не его класс. + # Через функцию-фабрику такое не получается устроить, потому что при + # добавлении расширения jinja2 ищет у его класса атрибут identifier. return self def parse(self, parser: Parser) -> List[nodes.Output]: @@ -1285,8 +1296,10 @@ class CalculateExtension(Extension): .format(str(error)), lineno=self.stream.current.lineno) - def check_conditions(self, conditions: List[Template]) -> NoReturn: + def check_conditions(self, conditions: List[Template]) -> None: for condition in conditions: + # TODO Добавить различие провала условие из синтаксической ошибки + # или при отрицательном условии. self.condition_result = False try: condition.render(__datavars__=self._datavars) @@ -1423,7 +1436,7 @@ class CalculateExtension(Extension): def _modify_variables(self, variable: List[str], namespace: NamespaceNode, new_value: Any, optype: int, target_file: Optional[str] = None, - modify_only: bool = True) -> NoReturn: + modify_only: bool = True) -> None: '''Метод для модификации значения переменной.''' variable_name = variable[-1] @@ -1457,7 +1470,7 @@ class CalculateExtension(Extension): # DEPRECATED def _modify_hash(self, variable_name: List[str], hash_variable: VariableNode, new_value, optype, - target_file: Optional[str] = None) -> NoReturn: + target_file: Optional[str] = None) -> None: '''Метод для модификации значения в переменной-хэше.''' value_name = variable_name[-1] hash_value = hash_variable.get_value().get_hash() @@ -1477,7 +1490,7 @@ class CalculateExtension(Extension): def _save_to_target(self, namespace_name: List[str], variable_name: str, value: Any, target_file: str - ) -> NoReturn: + ) -> None: '''Метод для добавления переменной в список переменных, значение которых было установлено через тег save и при этом должно быть сохранено в указанном файле: save.target_file.''' @@ -1677,7 +1690,7 @@ class CalculateExtension(Extension): class TemplateEngine: - def __init__(self, directory_path: Union[str, None] = None, + def __init__(self, directory_path: Optional[str] = None, datavars_module: Union[Datavars, NamespaceNode, Variables] = Variables(), @@ -1728,16 +1741,16 @@ class TemplateEngine: return self.parameters_processor.for_package @for_package.setter - def for_package(self, package: Package) -> NoReturn: + def for_package(self, package: Package) -> None: self.parameters_processor.for_package = package - def change_directory(self, directory_path: str) -> NoReturn: + def change_directory(self, directory_path: str) -> None: '''Метод для смены директории в загрузчике.''' self.environment.loader = FileSystemLoader(directory_path) def process_template(self, template_path: str, template_type: str, parameters: Optional[ParametersContainer] = None - ) -> NoReturn: + ) -> None: '''Метод для обработки файла шаблона, расположенного по указанному пути.''' if parameters is not None: @@ -1767,7 +1780,7 @@ class TemplateEngine: def process_template_from_string( self, string: str, template_type: int, parameters: Optional[ParametersContainer] = None - ) -> NoReturn: + ) -> None: '''Метод для обработки текста шаблона.''' if parameters is not None: self._parameters_object = parameters diff --git a/calculate/templates/template_processor.py b/calculate/templates/template_processor.py index 86d47fd..be633ea 100644 --- a/calculate/templates/template_processor.py +++ b/calculate/templates/template_processor.py @@ -43,7 +43,6 @@ from typing import ( List, Tuple, Iterator, - NoReturn, Optional, Callable ) @@ -123,21 +122,21 @@ class CalculateConfigFile: self._unsaved_changes = False return config_dictionary - def set_files_md5(self, file_path: str, file_md5: str) -> NoReturn: + def set_files_md5(self, file_path: str, file_md5: str) -> None: '''Метод для установки в config соответствия файла некоторой контрольной сумме.''' file_path = self._remove_chroot(file_path) self._config_dictionary[file_path] = file_md5 self._unsaved_changes = True - def remove_file(self, file_path: str) -> NoReturn: + def remove_file(self, file_path: str) -> None: '''Метод для удаления файла из config.''' file_path = self._remove_chroot(file_path) if file_path in self._config_dictionary: self._config_dictionary.pop(file_path) self._unsaved_changes = True - def compare_md5(self, file_path: str, file_md5: str) -> NoReturn: + def compare_md5(self, file_path: str, file_md5: str) -> None: '''Метод для сравнения хэш-суммы из config и некоторой заданной.''' file_path = self._remove_chroot(file_path) if file_path in self._config_dictionary: @@ -145,7 +144,7 @@ class CalculateConfigFile: else: return False - def save_changes(self) -> NoReturn: + def save_changes(self) -> None: '''Метод для записи изменений, внессенных в файл config.''' if not self._unsaved_changes: return @@ -168,7 +167,6 @@ class CalculateConfigFile: class TemplateWrapper: '''Класс связывающий шаблон с целевым файлом и определяющий параметры наложения шаблона, обусловленные состоянием целевого файла.''' - type_checks: Dict[int, Callable[[str], bool]] = {DIR: os.path.isdir, FILE: os.path.isfile} @@ -264,7 +262,7 @@ class TemplateWrapper: raise TemplateExecutorError("'format' parameter is not set" " file template.") - # Если по этому пути что-то есть -- проверяем конфликты. + # Если по этому пути что-то есть -- проверяем тип этого. if os.path.exists(target_file_path): for file_type, checker in self.type_checks.items(): if checker(target_file_path): @@ -312,7 +310,7 @@ class TemplateWrapper: # self.parameters.append == "replace"): # self.remove_original = True - def _check_type_conflicts(self) -> NoReturn: + def _check_type_conflicts(self) -> None: '''Метод для проверки конфликтов типов.''' if self.parameters.append == 'link': if self.parameters.force: @@ -386,7 +384,7 @@ class TemplateWrapper: raise TemplateTypeConflict("the target file is a directory" " while the template is a file") - def _check_package_collision(self) -> NoReturn: + def _check_package_collision(self) -> None: '''Метод для проверки на предмет коллизии, то есть конфликта пакета шаблона и целевого файла.''' if self.parameters.package: @@ -475,7 +473,9 @@ class TemplateWrapper: def _compare_packages(self, lpackage: PackageAtomName, rpackage: PackageAtomName ) -> Union[None, PackageAtomName]: - '''Метод, сравнивающий пакеты по их именам, возвращает старший.''' + '''Метод, сравнивающий пакеты по их именам, возвращает старший, если + пакеты с одинаковыми именами, но разными версиями, или None, если их + имена и категории не равны.''' if lpackage.category != rpackage.category: return None @@ -487,7 +487,7 @@ class TemplateWrapper: else: return rpackage - def _check_user_changes(self) -> NoReturn: + def _check_user_changes(self) -> None: '''Метод для проверки наличия пользовательских изменений в конфигурационных файлах.''' # Эта проверка только для файлов. @@ -520,8 +520,7 @@ class TemplateWrapper: # Путь к архивной версии файла. self.archive_path = self._get_archive_path(self.target_path) - self.contents_matching = (self.parameters.autoupdate - or self.parameters.force) + self.contents_matching = (self.parameters.autoupdate) if not self.protected: self.contents_matching = True @@ -545,6 +544,8 @@ class TemplateWrapper: # Если файл по целевому пути не относится к какому-либо пакету. # self.contents_matching = False pass + elif self.target_type is DIR and self.parameters.force: + self.contents_matching = True elif not self.contents_matching: # Если файл есть и он относится к текущему пакету. # Если по каким-то причинам уже нужно считать, что хэш-суммы @@ -613,7 +614,7 @@ class TemplateWrapper: return new_cfg_path - def remove_from_contents(self) -> NoReturn: + def remove_from_contents(self) -> None: '''Метод для удаления целевого файла из CONTENTS.''' if self.target_package is None: return @@ -623,13 +624,13 @@ class TemplateWrapper: elif self.template_type == FILE: self.target_package.remove_obj(self.target_path) - def clear_dir_contents(self) -> NoReturn: + def clear_dir_contents(self) -> None: '''Метод для удаления из CONTENTS всего содержимого директории после применения append = "clear".''' if self.template_type == DIR and self.target_package is not None: self.target_package.clear_dir(self.target_path) - def add_to_contents(self, file_md5: Optional[str] = None) -> NoReturn: + def add_to_contents(self, file_md5: Optional[str] = None) -> None: '''Метод для добавления целевого файла в CONTENTS.''' if self.target_package is None: return @@ -652,7 +653,7 @@ class TemplateWrapper: elif self.template_type == FILE: self.target_package.add_obj(source_path, file_md5=file_md5) - def update_contents_from_list(self, changed_list: dict) -> NoReturn: + def update_contents_from_list(self, changed_list: dict) -> None: '''Метод для изменения CONTENTS по списку измененных файлов.''' print("UPDATE CONTENTS FROM LIST") if self.target_package is None: @@ -676,7 +677,7 @@ class TemplateWrapper: self.target_package.add_dir(file_path) @classmethod - def _set_protected(cls, chroot_path: str) -> NoReturn: + def _set_protected(cls, chroot_path: str) -> None: '''Метод для получения множества защищенных директорий.''' cls._protected_set = set() cls._unprotected_set = set() @@ -699,7 +700,7 @@ class TemplateWrapper: cls._protected_is_set = True - def save_changes(self) -> NoReturn: + def save_changes(self) -> None: '''Метод для сохранения изменений внесенных в CONTENTS.''' if self.target_package: self.target_package.remove_empty_directories() @@ -864,7 +865,7 @@ class TemplateExecutor: return self.executor_output - def save_changes(self) -> NoReturn: + def save_changes(self) -> None: '''Метод для сохранения чего-нибудь после выполнения всех шаблонов.''' # Пока сохраняем только получившееся содержимое config-файла. self.calculate_config_file.save_changes() @@ -892,7 +893,7 @@ class TemplateExecutor: template_object.add_to_contents() def _append_remove_directory(self, template_object: TemplateWrapper - ) -> NoReturn: + ) -> None: '''Метод описывающий действия для append = "remove", если шаблон -- директория. Удаляет директорию со всем содержимым, если она есть.''' if template_object.target_type is not None: @@ -914,11 +915,11 @@ class TemplateExecutor: template_object.remove_from_contents() def _append_skip_directory(self, - template_object: TemplateWrapper) -> NoReturn: + template_object: TemplateWrapper) -> None: pass def _append_clear_directory(self, - template_object: TemplateWrapper) -> NoReturn: + template_object: TemplateWrapper) -> None: '''Метод описывающий действия для append = "clear", если шаблон -- директория. Удаляет все содержимое директории, если она есть.''' if template_object.target_type is not None: @@ -949,7 +950,7 @@ class TemplateExecutor: template_object.clear_dir_contents() def _append_link_directory(self, template_object: TemplateWrapper - ) -> NoReturn: + ) -> None: '''Метод описывающий действия для append = "link", если шаблон -- директория. Создает ссылку на директорию, если она есть.''' self._link_directory(template_object.parameters.source, @@ -974,7 +975,7 @@ class TemplateExecutor: template_object.add_to_contents() def _append_replace_directory(self, template_object: TemplateWrapper - ) -> NoReturn: + ) -> None: '''Метод описывающий действия для append = "replace", если шаблон -- директория. Очищает директорию или создает, если ее нет.''' if template_object.target_type is None: @@ -1006,7 +1007,7 @@ class TemplateExecutor: def _append_join_file(self, template_object: TemplateWrapper, join_before: bool = False, replace: bool = False - ) -> NoReturn: + ) -> None: '''Метод описывающий действия при append = "join", если шаблон -- файл. Объединяет шаблон с целевым файлом.''' output_path = template_object.output_path @@ -1105,6 +1106,7 @@ class TemplateExecutor: chroot_path=self.chroot_path) # Удаляем форматный объект входного файла. # del(parsed_template) + # Если исполняемый формат выдал список измененных файлов для # изменения CONTENTS и при этом задан пакет -- обновляем # CONTENTS. @@ -1279,32 +1281,32 @@ class TemplateExecutor: chown = self.file_default_parameters.get('chown', False) return chown - def _append_after_file(self, template_object: TemplateWrapper) -> NoReturn: + def _append_after_file(self, template_object: TemplateWrapper) -> None: '''Метод описывающий действия при append = "after", если шаблон -- файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся в конец файла и в конец каждой секции файла.''' self._append_join_file(template_object, join_before=False) def _append_before_file(self, template_object: TemplateWrapper - ) -> NoReturn: + ) -> None: '''Метод описывающий действия при append = "after", если шаблон -- файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся в начало файла и в начало каждой секции файла.''' self._append_join_file(template_object, join_before=True) - def _append_skip_file(self, template_object: TemplateWrapper) -> NoReturn: + def _append_skip_file(self, template_object: TemplateWrapper) -> None: '''Метод описывающий действия при append = "skip". Пока никаких действий.''' pass def _append_replace_file(self, template_object: TemplateWrapper - ) -> NoReturn: + ) -> None: '''Метод описывающий действия при append = "replace", если шаблон -- файл. Очищает файл и затем накладывает на него шаблон.''' self._append_join_file(template_object, replace=True) def _append_remove_file(self, template_object: TemplateWrapper - ) -> NoReturn: + ) -> None: '''Метод описывающий действия при append = "remove", если шаблон -- файл. Удаляет файл.''' if template_object.target_type is not None: @@ -1320,7 +1322,7 @@ class TemplateExecutor: if self.dbpkg: template_object.remove_from_contents() - def _append_clear_file(self, template_object: TemplateWrapper) -> NoReturn: + def _append_clear_file(self, template_object: TemplateWrapper) -> None: '''Метод описывающий действия при append = "clear", если шаблон -- файл. Очищает файл.''' if template_object.target_type is not None: @@ -1342,7 +1344,7 @@ class TemplateExecutor: if self.dbpkg: template_object.add_to_contents() - def _append_link_file(self, template_object: TemplateWrapper) -> NoReturn: + def _append_link_file(self, template_object: TemplateWrapper) -> None: '''Метод описывающий действия при append = "link", если шаблон -- файл. Создает ссылку на файл, указанный в параметре source.''' input_path = template_object.input_path @@ -1424,7 +1426,7 @@ class TemplateExecutor: return hashlib.md5(source_path.encode()).hexdigest() def _create_directory(self, template_object: TemplateWrapper, - path_to_create: Optional[str] = None) -> NoReturn: + path_to_create: Optional[str] = None) -> None: '''Метод для создания директории и, при необходимости, изменения владельца и доступа все директорий на пути к целевой.''' if path_to_create is None: @@ -1481,7 +1483,7 @@ class TemplateExecutor: 'Failed to create directory: {}, reason: {}'. format(create_path, str(error))) - def _remove_directory(self, target_path: str) -> NoReturn: + def _remove_directory(self, target_path: str) -> None: '''Метод для удаления директории.''' if os.path.exists(target_path): if os.path.isdir(target_path): @@ -1505,7 +1507,7 @@ class TemplateExecutor: "reason: {1}").format(target_path, error_message)) - def _clear_directory(self, target_path: str) -> NoReturn: + def _clear_directory(self, target_path: str) -> None: '''Метод для очистки содержимого целевой директории.''' if os.path.exists(target_path): if os.path.isdir(target_path): @@ -1526,9 +1528,9 @@ class TemplateExecutor: error_message)) def _link_directory(self, source: str, target_path: str, - force: bool = False) -> NoReturn: + force: bool = False) -> None: '''Метод для создания по целевому пути ссылки на директорию - расположенную на пути, указанному в source.''' + расположенную на пути, указанном в source.''' try: os.symlink(source, target_path, target_is_directory=True) except OSError as error: @@ -1536,7 +1538,7 @@ class TemplateExecutor: "failed to create symlink: {0} -> {1}, reason: {2}". format(target_path, source, str(error))) - def _remove_file(self, target_path: str) -> NoReturn: + def _remove_file(self, target_path: str) -> None: '''Метод для удаления файлов.''' if os.path.exists(target_path): if os.path.isfile(target_path): @@ -1567,7 +1569,7 @@ class TemplateExecutor: "reason: {1}").format(target_path, error_message)) - def _clear_file(self, target_path: str) -> NoReturn: + def _clear_file(self, target_path: str) -> None: '''Метод для очистки файлов.''' if os.path.exists(target_path): if os.path.isfile(target_path): @@ -1586,7 +1588,7 @@ class TemplateExecutor: "reason: {1}").format(target_path, error_message)) - def _link_file(self, source: str, target_path: str) -> NoReturn: + def _link_file(self, source: str, target_path: str) -> None: '''Метод для создания по целевому пути ссылки на файл расположенный на пути, указанному в source.''' try: @@ -1596,7 +1598,7 @@ class TemplateExecutor: "failed to create symlink to the file: {0} -> {1}, reason: {2}". format(target_path, source, str(error))) - def _run_template(self, template_object: TemplateWrapper) -> NoReturn: + def _run_template(self, template_object: TemplateWrapper) -> None: '''Метод для сохранения текста шаблонов, который должен быть исполнен интерпретатором указанным в run прямо во время обработки шаблонов.''' text_to_run = template_object.template_text @@ -1634,7 +1636,7 @@ class TemplateExecutor: " interpreter '{}', reason: {}"). format(interpreter, str(error))) - def _exec_template(self, template_object: TemplateWrapper) -> NoReturn: + def _exec_template(self, template_object: TemplateWrapper) -> None: '''Метод для сохранения текста шаблонов, который должен быть исполнен интерпретатором указанным в exec после выполнения всех прочих шаблонов. ''' @@ -1720,7 +1722,7 @@ class TemplateExecutor: format(interpreter, str(error))) def _chown_directory(self, target_path: str, chown_value: dict - ) -> NoReturn: + ) -> None: """Метод для смены владельца директории.""" try: if os.path.exists(target_path): @@ -1738,7 +1740,7 @@ class TemplateExecutor: chown_value['gid']), str(error))) - def _chmod_directory(self, target_path: str, chmod_value: int) -> NoReturn: + def _chmod_directory(self, target_path: str, chmod_value: int) -> None: '''Метод для смены прав доступа к директории.''' if isinstance(chmod_value, tuple) and not chmod_value[1]: chmod_value = chmod_value[0] @@ -1759,7 +1761,7 @@ class TemplateExecutor: 'Can not chmod directory: {0}, reason: {1}'. format(target_path, str(error))) - def _chown_file(self, target_path: str, chown_value: dict) -> NoReturn: + def _chown_file(self, target_path: str, chown_value: dict) -> None: '''Метод для смены владельца файла.''' try: if os.path.exists(target_path): @@ -1777,7 +1779,7 @@ class TemplateExecutor: chown_value['gid']), str(error))) - def _chmod_file(self, target_path: str, chmod_value: int) -> NoReturn: + def _chmod_file(self, target_path: str, chmod_value: int) -> None: '''Метод для смены прав доступа к директории.''' try: if not os.path.exists(target_path): @@ -1802,13 +1804,15 @@ class TemplateExecutor: current_mode: Optional[int] = None) -> int: '''Метод для наложения X-маски, необходимой для получения значения chmod, c учетом возможности наличия в нем значения "X".''' - if not chmod[1]: - return chmod[0] + chmod, x_mask = chmod + + if not x_mask: + return chmod if current_mode is None: - return chmod[0] ^ chmod[1] + return chmod ^ x_mask else: - return chmod[0] ^ (current_mode & chmod[1]) + return chmod ^ (current_mode & x_mask) def _get_file_mode(self, file_path: str) -> int: '''Метод для получения прав доступа для указанного файла.''' @@ -1847,7 +1851,7 @@ class TemplateExecutor: if self.chroot_path == '/': group_name = grp.getgrgid(gid).gr_name else: - group_name = self._get_group_name_form_gid(gid) + group_name = self._get_group_name_from_gid(gid) except (TypeError, KeyError): group_name = str(gid) @@ -1875,7 +1879,7 @@ class TemplateExecutor: else: return str(uid) - def _get_group_name_form_gid(self, gid: int) -> str: + def _get_group_name_from_gid(self, gid: int) -> str: '''Метод для получения названия группы по его gid.''' group_file_path = os.path.join(self.chroot_path, 'etc/group') group_dictionary = dict() @@ -1927,7 +1931,7 @@ class DirectoryTree: self.base_directory = base_directory self._tree = {} - def update_tree(self, tree: dict) -> NoReturn: + def update_tree(self, tree: dict) -> None: '''Метод, инициирующий наложение заданного дерева каталогов на данный экземпляр дерева.''' self._update(self._tree, tree) @@ -1943,7 +1947,7 @@ class DirectoryTree: original_tree[parent] = child return original_tree - def show_tree(self) -> NoReturn: + def show_tree(self) -> None: pprint(self._tree) def get_directory_tree(self, directory: str) -> "DirectoryTree": @@ -1956,13 +1960,13 @@ class DirectoryTree: directory_tree._tree = self._tree[directory] return directory_tree - def __getitem__(self, name: str) -> dict: + def __getitem__(self, name: str) -> Union[None, dict]: if name in self._tree: return self._tree[name] else: return None - def __setitem__(self, name: str, value: Union[None, dict]) -> NoReturn: + def __setitem__(self, name: str, value: Union[None, dict]) -> None: self._tree[name] = value def __iter__(self) -> Iterator[str]: @@ -2149,7 +2153,7 @@ class DirectoryProcessor: return path_to_add def _add_package_to_group(self, group_name: str, - package_atom: str) -> NoReturn: + package_atom: str) -> None: try: groups_namespace = self.datavars_module.main.cl.groups except (VariableNotFoundError, AttributeError): @@ -2230,7 +2234,7 @@ class DirectoryProcessor: return -1 return -2 - def _make_current_template_var(self) -> NoReturn: + def _make_current_template_var(self) -> None: var_path = ['main', 'cl'] namespace = self.datavars_module @@ -2251,7 +2255,7 @@ class DirectoryProcessor: else: namespace['current_template'] = "" - def process_template_directories(self) -> NoReturn: + def process_template_directories(self) -> None: '''Метод для обхода шаблонов, содержащихся в каталогах из main.cl_template.path.''' # Режим заполнения очередей директорий пакетов, необходимых для более @@ -2319,12 +2323,11 @@ class DirectoryProcessor: PackageCreator.save_all() return self.template_executor.changed_files - def _run_template_from_base_directory( - self, template_names: List[str], - base_directory: str, - package: Optional[Package] = None, - directory_tree: Optional[dict] = None - ) -> NoReturn: + def _run_template_from_base_directory(self, template_names: List[str], + base_directory: str, + package: Optional[Package] = None, + directory_tree: Optional[dict] = None + ) -> None: '''Метод для запуска шаблонов файлов находящихся в базовой директории. ''' self.template_engine.change_directory(base_directory) @@ -2380,6 +2383,7 @@ class DirectoryProcessor: else: template_package = package + # Дефолтные значения разные для упрощения написания шаблонов. if not parameters.run and not parameters.exec: if not parameters.format: parameters.set_parameter({'format': 'raw'}) @@ -2396,7 +2400,7 @@ class DirectoryProcessor: template_text=template_text, package=template_package) - def _execute_handlers(self) -> NoReturn: + def _execute_handlers(self) -> None: '''Метод для запуска обработчиков добавленных в очередь обработчиков с помощью параметра notify.''' self.output.set_info('Processing handlers...') @@ -2462,7 +2466,7 @@ class DirectoryProcessor: FILE, handler_path, template_text=handler_text) - def _merge_packages(self) -> NoReturn: + def _merge_packages(self) -> None: '''Метод для выполнения шаблонов относящихся к пакетам, указанным во всех встреченных значениях параметра merge.''' not_merged_packages = [] @@ -2527,7 +2531,7 @@ class DirectoryProcessor: else: self.output.set_success('All packages are merged.') - def _run_exec_files(self) -> NoReturn: + def _run_exec_files(self) -> None: '''Метод для выполнения скриптов, полученных в результате обработки шаблонов с параметром exec.''' for exec_file_path, exec_info in\ @@ -2554,7 +2558,7 @@ class DirectoryProcessor: current_target_path: str, directory_parameters: ParametersContainer, directory_tree: Union[dict, DirectoryTree] = {}, - package: Optional[Package] = None) -> NoReturn: + package: Optional[Package] = None) -> None: '''Метод для рекурсивного обхода директорий с шаблонами, а также, при необходимости, заполнения деревьев директорий шаблонов, с помощью которых далее выполняются шаблоны пакетов из merge.''' @@ -2805,7 +2809,6 @@ class DirectoryProcessor: if self.fill_trees: directory_tree = {} - return def _scan_directory(self, directory_path: str ) -> Tuple[List[str], List[str]]: diff --git a/calculate/utils/calculateini.py b/calculate/utils/calculateini.py deleted file mode 100644 index 54a9933..0000000 --- a/calculate/utils/calculateini.py +++ /dev/null @@ -1,89 +0,0 @@ -from pyparsing import Literal, Word, ZeroOrMore, Group, Dict, Optional,\ - restOfLine, empty, printables, OneOrMore, oneOf, nums,\ - lineno, line, col, Keyword, SkipTo, LineEnd, Combine -from enum import Enum - - -class CalculateIniParser: - '''Класс парсера calculate.ini файлов.''' - class Define(Enum): - assign = 0 - append = 1 - remove = 2 - - def __init__(self): - self.operations = {"=": self.Define.assign, - "+=": self.Define.append, - "-=": self.Define.remove} - lbrack = Literal("[") - rbrack = Literal("]") - - # comma = Literal(",").suppress() - comment_symbol = Literal(';') | Literal('#') - - # Define = self.Define - value_operation = (Literal("=") | Combine(Literal("+") + Literal("=")) - | Combine(Literal("-") + Literal("="))) - - comment = comment_symbol + Optional(restOfLine) - - section_name = Word(printables+'\t', excludeChars='[]') - - value_name = Word(printables+'\t', excludeChars='=-+') - - # non_comma = Word(printables+'\t', excludeChars=',') - clear_section = lbrack.suppress() + Group(empty) + rbrack.suppress() - - section_start = Group(OneOrMore(lbrack.suppress() + section_name - + rbrack.suppress()) - + (clear_section | ~lbrack()) - + LineEnd().suppress()) - - # Если содержимое ini-файла не предваряется заголовком секции, - # значит эта строка ошибочна. - unexpected = Group(~section_start + SkipTo(LineEnd(), - include=True))("error") - unexpected.setParseAction(self._unexpected_token) - - key_value = (~lbrack + value_name - + value_operation + empty - + restOfLine + LineEnd().suppress()) - - def strip_key_value(tokens): - tokens[0] = tokens[0].strip() - tokens[1] = tokens[1].strip() - - key_value.setParseAction(strip_key_value) - - self.ini_section_parser = (section_start - + Group(ZeroOrMore( - Group(key_value | unexpected))) - | unexpected) - self.ini_section_parser.ignore(comment) - - def _unexpected_token(self, string, location, tokens): - '''Метод вызываемый парсером, если обнаружена некорректная строка, - предназначен для получения некорректной строки и ее дальнейшего - разбора.''' - error_line = line(location, string).strip() - if error_line: - return (error_line, lineno(location, string), - col(location, string)) - - def parse(self, data): - for tokens, start, end in self.ini_section_parser.scanString(data): - if tokens.getName() == "error": - print('error tokens: {}'.format(tokens)) - yield {'error': tokens} - - section, defkeys = tokens - - section_list = section.asList() - if section_list[-1] == []: - self.clear_section(section_list[:-1]) - yield {'clear_section': (section_list[:-1], )} - - self.start_section(section.asList()) - for defkey in defkeys: - yield {'define_key': (section.asList(), defkey[0], defkey[2], - self.operations[defkey[1]])} diff --git a/calculate/utils/contents_template b/calculate/utils/contents_template deleted file mode 100644 index 2024fee..0000000 --- a/calculate/utils/contents_template +++ /dev/null @@ -1,14 +0,0 @@ -{% for file_name, contents_values in document_dictionary.items() %} -{% if contents_values is mapping %} -{% set contents_values = (contents_values.values()|list) %} -{% else %} -{% set contents_values = (contents_values[0]|list) %} -{% set file_name = file_name[-1] %} -{% endif %} -{% if contents_values[0] == 'sym' %} -{{ contents_values[0] }} {{ file_name }} -> {{ contents_values[1:]|join(' ') }} -{% else %} -{{ contents_values[0] }} {{ file_name }}{% if contents_values[1:] %} {{ contents_values[1:]|join(' ') }}{% endif %} - -{% endif %} -{% endfor %} diff --git a/calculate/utils/gentoo.py b/calculate/utils/gentoo.py index bc506ec..6820724 100644 --- a/calculate/utils/gentoo.py +++ b/calculate/utils/gentoo.py @@ -1,22 +1,23 @@ import os import re +from typing import Dict from calculate.utils.files import read_file_lines class ProfileWalker: '''Объект обходящий все директории профиля через parent файлы.''' - def __init__(self, filename, repositories): - self.repositories = repositories - self.filename = filename - self.re_reppath = re.compile("^({0})+:".format( + def __init__(self, filename: str, repositories: Dict[str, str]): + self.repositories: Dict[str, str] = repositories + self.filename: str = filename + self.re_reppath: re.Pattern = re.compile("^({0})+:".format( "|".join(self.repositories.keys()))) - def interpolate(self, path): + def interpolate(self, path: str): def subfunc(m): return "{0}/profiles/".format(self.repositories.get(m.group(1))) return self.re_reppath.sub(subfunc, path) - def find(self, directory): + def find(self, directory: str): '''Метод для поиска по профилю всех файлов с именем, указанным в self.filename.''' parent_file_path = os.path.join(directory, "parent") diff --git a/calculate/utils/ldap.py b/calculate/utils/ldap.py deleted file mode 100644 index e69de29..0000000 diff --git a/calculate/utils/package.py b/calculate/utils/package.py index 16cb436..881a692 100644 --- a/calculate/utils/package.py +++ b/calculate/utils/package.py @@ -7,7 +7,6 @@ from collections import OrderedDict from .files import read_file, read_link, join_paths, FilesError from typing import ( Generator, - NoReturn, Optional, Dict, Tuple, @@ -15,16 +14,6 @@ from typing import ( List, Any ) -from pyparsing import ( - Literal, - Regex, - Word, - nums, - alphanums, - LineEnd, - SkipTo, - ) -from jinja2 import PackageLoader, Environment from calculate.utils.tools import Singleton import hashlib @@ -59,6 +48,10 @@ class PackageNotFound(Exception): class PackageCreator(type): + """Метакласс для создания классов пакетов, напоминающий метакласс + Singleton. Следит за тем, чтобы для каждого пакета создавался только один + экземляр объекта Package.""" + # TODO Перенести этот функционал в класс Package. _instances: Dict["PackageAtomName", "Package"] = {} def __call__(cls, *args, **kwargs): @@ -74,14 +67,14 @@ class PackageCreator(type): return cls._instances[args[0]] @classmethod - def save_all(cls) -> NoReturn: + def save_all(cls) -> None: for atom, pkg in cls._instances.items(): pkg.remove_empty_directories() pkg.write_contents_file() @classmethod - def clear_instances(cls) -> NoReturn: - """Метод для очистки текущего кэша пакетный объектов. + def clear_instances(cls) -> None: + """Метод для очистки текущего кэша пакетных объектов. Необходим при тестировании.""" cls._instances.clear() @@ -370,62 +363,6 @@ class ContentsParser(metaclass=Singleton): return "\n".join(lines) -class OldContentsParser(metaclass=Singleton): - def __init__(self): - sym_keyword = Literal('sym') - dir_keyword = Literal('dir') - obj_keyword = Literal('obj') - - symlink_arrow = Literal('->') - - file_path = Regex(r'\S+') - - time_value = Word(nums) - - md5 = Word(alphanums) - - sym_line = (sym_keyword('type') + file_path('path') - + symlink_arrow.suppress() + file_path('target') - + time_value('mtime') + LineEnd().suppress()) - - dir_line = (dir_keyword('type') + file_path('path') - + LineEnd().suppress()) - - obj_line = (obj_keyword('type') + file_path('path') - + md5('md5') + time_value('mtime') + LineEnd().suppress()) - unexpected = SkipTo(LineEnd(), include=True) - - self._parser = (dir_line | sym_line | - obj_line | unexpected('unexpected') - ).setParseAction(self._parser_method) - - def _parser_method(self, parse_result): - if parse_result.getName() == 'unexpected': - return [None] - result_dict = parse_result.asDict() - path = result_dict.pop('path') - value = ({path: result_dict}) - return value - - def parse(self, contents_text: str) -> OrderedDict: - output_dictionary = OrderedDict() - for tokens, start, end in self._parser.scanString(contents_text): - parse_result = tokens[0] - if parse_result is None: - continue - output_dictionary.update(parse_result) - return output_dictionary - - def render(self, contents_dictionary: dict) -> str: - file_loader = PackageLoader('calculate', 'utils') - environment = Environment(loader=file_loader, - trim_blocks=True, - lstrip_blocks=True) - template = environment.get_template('contents_template') - text = template.render(document_dictionary=contents_dictionary) - return text - - class PackageAtomName: '''Класс для хранения результата определения пакета. Для определения пакета использует путь к его pkg директории.''' @@ -686,7 +623,7 @@ class PackageAtomParser: return atom_dictionary def _check_slot_value(self, pkg_path: str, - atom_dictionary: dict) -> NoReturn: + atom_dictionary: dict) -> None: '''Метод для проверки полученного из параметра package значения slot. ''' if atom_dictionary['slot']: @@ -699,7 +636,7 @@ class PackageAtomParser: errno=NOTEXIST) def _check_use_flags_value(self, pkg_path: str, - atom_dictionary: dict) -> NoReturn: + atom_dictionary: dict) -> None: '''Метод для проверки полученных из параметра package значений use-флагов.''' if atom_dictionary['use_flags']: @@ -742,7 +679,7 @@ class PackageAtomParser: yield path def _check_version(self, atom_dictionary: dict, pkg_version: Version - ) -> NoReturn: + ) -> None: condition = atom_dictionary['condition'] if condition == '=': @@ -769,6 +706,7 @@ class PackageAtomParser: def get_file_package(self, file_path: str) -> PackageAtomName: '''Метод для определения пакета, которому принадлежит файл.''' # Удаляем часть пути соответствующую chroot_path + # TODO предусмотреть кэширование результата. if self.chroot_path != '/' and file_path.startswith(self.chroot_path): file_path = file_path[len(self.chroot_path):] @@ -919,7 +857,7 @@ class Package(metaclass=PackageCreator): else: return False - def write_contents_file(self) -> NoReturn: + def write_contents_file(self) -> None: '''Метод для записи файла CONTENTS.''' with open(self.contents_file_path, 'w') as contents_file: contents_text = self.render_contents_file() @@ -943,7 +881,7 @@ class Package(metaclass=PackageCreator): return self.contents_dictionary[file_path]['type'] return None - def sort_contents_dictionary(self) -> NoReturn: + def sort_contents_dictionary(self) -> None: '''Метод для сортировки словаря, полученного в результате разбора и изменения CONTENTS-файла.''' tree = {} @@ -980,7 +918,7 @@ class Package(metaclass=PackageCreator): if level[part]: yield from self._make_paths(part_path, level[part]) - def add_dir(self, file_name: str) -> NoReturn: + def add_dir(self, file_name: str) -> None: '''Метод для добавления в CONTENTS директорий.''' file_name = self.remove_chroot_path(file_name) @@ -992,7 +930,7 @@ class Package(metaclass=PackageCreator): self.contents_dictionary[file_name] = contents_item def add_sym(self, file_name: str, target_path: Optional[str] = None, - mtime: Optional[str] = None) -> NoReturn: + mtime: Optional[str] = None) -> None: '''Метод для добавления в CONTENTS символьных ссылок.''' real_path = file_name @@ -1018,7 +956,7 @@ class Package(metaclass=PackageCreator): self.contents_dictionary[file_name] = contents_item def add_obj(self, file_name: str, file_md5: Optional[str] = None, - mtime: Optional[str] = None) -> NoReturn: + mtime: Optional[str] = None) -> None: '''Метод для добавления в CONTENTS обычных файлов как obj.''' real_path = file_name file_name = self.remove_chroot_path(file_name) @@ -1043,7 +981,7 @@ class Package(metaclass=PackageCreator): 'mtime': mtime}) self.contents_dictionary[file_name] = contents_item - def add_file(self, file_name: str) -> NoReturn: + def add_file(self, file_name: str) -> None: '''Метод для добавления в CONTENTS файла любого типа.''' if file_name != '/': real_path = file_name @@ -1197,7 +1135,7 @@ class Package(metaclass=PackageCreator): return ''.format(self.package_name.category, self.package_name.fullname) - # def __del__(self) -> NoReturn: + # def __del__(self) -> None: # if self.autosave and os.path.exists(self.contents_file_path): # self.remove_empty_directories() # self.write_contents_file() diff --git a/calculate/utils/system.py b/calculate/utils/system.py index 2886f86..f86e995 100644 --- a/calculate/utils/system.py +++ b/calculate/utils/system.py @@ -1,6 +1,7 @@ from calculate.utils.files import grep_file import os + class SystemType: """ Тип контейнера текущей системы @@ -21,12 +22,12 @@ class SystemType: if grep_file("/proc/cpuinfo", "UML"): return cls.Uml elif grep_file("/proc/self/status", - "(s_context|VxID):\s*[1-9]"): + r"(s_context|VxID):\s*[1-9]"): return cls.VServer elif (os.path.exists("/proc/vz/veinfo") and not os.path.exists("/proc/vz/version")): return cls.OpenVZ - elif grep_file("/proc/self/status", "envID:\s*[1-9]"): + elif grep_file("/proc/self/status", r"envID:\s*[1-9]"): return cls.OpenVZ elif grep_file("/proc/1/environ", "container=lxc"): return cls.LXC diff --git a/calculate/utils/tools.py b/calculate/utils/tools.py index 47e8d17..02d0cb5 100644 --- a/calculate/utils/tools.py +++ b/calculate/utils/tools.py @@ -117,12 +117,15 @@ def get_traceback_caller(exception_type, exception_object, if module_name.endswith('.py'): module_name = module_name[:-3] full_module_name = [module_name] + while (module_path and module_path != '/' and not module_path.endswith('site-packages')): module_path, package_name = os.path.split(module_path) full_module_name.insert(0, package_name) + if module_path.endswith('site-packages'): module_name = '.'.join(full_module_name) + return module_name, line_number diff --git a/calculate/variables/datavars.py b/calculate/variables/datavars.py index 2eca111..729a6e7 100644 --- a/calculate/variables/datavars.py +++ b/calculate/variables/datavars.py @@ -1,13 +1,15 @@ # vim: fileencoding=utf-8 # import re -import ast +# import ast import dis from contextlib import contextmanager -from inspect import signature, getsource -from types import FunctionType, LambdaType +# from inspect import signature, getsource +from inspect import signature +# from types import FunctionType, LambdaType +from types import FunctionType from calculate.utils.tools import Singleton -from typing import List, Any, Union, Generator, Callable +from typing import List, Any, Union, Generator, Callable, Optional class DependenceError(Exception): @@ -436,7 +438,7 @@ class VariableWrapper: class DependenceSource: '''Класс зависимости как источника значения переменной.''' def __init__(self, variables: tuple, - depend: Union[Callable, None] = None): + depend: Optional[Callable] = None): self._args: Union[tuple, list] = variables self.depend_function: Union[Callable, None] = depend self._subscriptions: set = set() @@ -736,7 +738,7 @@ class VariableNode: subscriber._invalidate() @contextmanager - def _start_calculate(self): + def _start_calculate(self) -> Generator["VariableNode", None, None]: '''Менеджер контекста устанавливающий флаг, указывающий, что данная переменная в состоянии расчета. В данном случае необходима только для того, чтобы при получении значения параметра внутри метода для расчета @@ -782,7 +784,7 @@ class VariableNode: class NamespaceNode: '''Класс ноды соответствующей пространству имен в дереве переменных.''' def __init__(self, name: str = '', - parent: Union["NamespaceNode", None] = None): + parent: Optional["NamespaceNode"] = None): self._name = name self._variables = dict() self._namespaces = dict() @@ -943,9 +945,10 @@ class FormatAPI(metaclass=Singleton): def __call__(self, string: str) -> "Dependence": vars_list = [] - def subfunc(matchobj): + def subfunc(matchobj: re.Match): vars_list.append(matchobj.group(0)[1:-1].strip()) return '{}' + format_string = self.pattern.sub(subfunc, string) def depend_function(*args): @@ -1013,7 +1016,7 @@ class NamespaceAPI(metaclass=Singleton): '''Класс для создания пространств имен при задании переменных через python-скрипты.''' def __init__(self, var_fabric: VariableAPI, - dependence_fabric: DependenceAPI(), + dependence_fabric: DependenceAPI, datavars_root=NamespaceNode('')): self._datavars = datavars_root self.current_namespace = self._datavars @@ -1028,7 +1031,7 @@ class NamespaceAPI(metaclass=Singleton): self._dependence_fabric.datavars_root = self._datavars @property - def datavars(self): + def datavars(self) -> NamespaceNode: '''Метод для получения корневого пространства имен, через которое далее можно получить доступ к переменным.''' return self._datavars @@ -1058,7 +1061,8 @@ class NamespaceAPI(metaclass=Singleton): self._dependence_fabric.current_namespace = namespace @contextmanager - def __call__(self, namespace_name: str): + def __call__(self, namespace_name: str + ) -> Generator["NamespaceAPI", None, None]: '''Метод для создания пространств имен с помощью with.''' if namespace_name not in self.current_namespace._namespaces: namespace = NamespaceNode(namespace_name, diff --git a/calculate/variables/loader.py b/calculate/variables/loader.py index a1087b9..c47e5c9 100644 --- a/calculate/variables/loader.py +++ b/calculate/variables/loader.py @@ -5,6 +5,7 @@ import logging import importlib import importlib.util from jinja2 import Environment, PackageLoader +from typing import Dict, Optional, Any, List, Generator from calculate.variables.datavars import ( NamespaceNode, VariableNode, @@ -22,11 +23,12 @@ from calculate.utils.gentoo import ProfileWalker from calculate.utils.files import read_file, FilesError from calculate.utils.tools import Singleton from pyparsing import ( + ParseResults, Literal, Word, ZeroOrMore, Group, - Optional, + Optional as OptionalPattern, restOfLine, empty, printables, @@ -65,14 +67,12 @@ class CalculateIniParser(metaclass=Singleton): lbrack = Literal("[") rbrack = Literal("]") - # comma = Literal(",").suppress() comment_symbol = Literal(';') | Literal('#') - # Define = self.Define value_operation = (Literal("=") | Combine(Literal("+") + Literal("=")) | Combine(Literal("-") + Literal("="))) - comment = comment_symbol + Optional(restOfLine) + comment = comment_symbol + OptionalPattern(restOfLine) section_name = (lbrack.suppress() + (~Word(nums) + Word(printables+'\t', @@ -81,7 +81,6 @@ class CalculateIniParser(metaclass=Singleton): value_name = Word(printables+'\t', excludeChars='=-+') - # non_comma = Word(printables+'\t', excludeChars=',') clear_section = lbrack.suppress() + Group(empty) + rbrack.suppress() row_index = lbrack.suppress() + Word(nums) + rbrack.suppress() @@ -93,7 +92,8 @@ class CalculateIniParser(metaclass=Singleton): + (row_index | clear_section | ~lbrack) + LineEnd().suppress()) - def add_lineno(string, location, tokens): + def add_lineno(string: str, location: int, + tokens: ParseResults) -> None: tokens.append(lineno(location, string)) section_start = (namespace_start('namespace') | @@ -110,7 +110,8 @@ class CalculateIniParser(metaclass=Singleton): + value_operation + empty + restOfLine + LineEnd().suppress()) - def process_key_value(string, location, tokens): + def process_key_value(string: str, location: int, + tokens: ParseResults) -> None: tokens[0] = tokens[0].strip() tokens[1] = tokens[1].strip() tokens.append(lineno(location, string)) @@ -124,7 +125,7 @@ class CalculateIniParser(metaclass=Singleton): | unexpected) self.ini_section_parser.ignore(comment) - def parse(self, data: str): + def parse(self, data: str) -> Generator[Dict[str, tuple], None, None]: for tokens, start, end in self.ini_section_parser.scanString(data): if tokens.getName() == "error": if tokens[1].strip(): @@ -161,7 +162,8 @@ class CalculateIniParser(metaclass=Singleton): yield {'start_table': (table_list, table_values, section_lineno)} - def _unexpected_token(self, string, location, tokens): + def _unexpected_token(self, string: str, location: int, + tokens: ParseResults) -> list: '''Метод вызываемый парсером, если обнаружена некорректная строка, предназначен для получения некорректной строки и ее дальнейшего разбора.''' @@ -173,14 +175,14 @@ class NamespaceIniFiller: из calculate.ini файла.''' available_sections = {'custom'} - def __init__(self, restrict_creation=True): + def __init__(self, restrict_creation: bool = True): self.ini_parser = CalculateIniParser() self._errors = [] # Флаги, определяющие возможность создания новых переменных и новых # пространств имен в данном пространстве имен. - self.restricted = restrict_creation - self.modify_only = False + self.restricted: bool = restrict_creation + self.modify_only: bool = False def fill(self, namespace: NamespaceNode, ini_file_text: str) -> None: '''Метод для разбора calculate.ini файла и добавления всех его @@ -192,11 +194,12 @@ class NamespaceIniFiller: for parsed_line in self.ini_parser.parse(ini_file_text): self._line_processor(**parsed_line) - def _line_processor(self, start_section=None, - clear_section=None, - start_table=None, - define_key=None, - error=None, **kwargs): + def _line_processor(self, start_section: Optional[tuple] = None, + clear_section: Optional[tuple] = None, + start_table: Optional[tuple] = None, + define_key: Optional[tuple] = None, + error: Optional[tuple] = None, + **kwargs) -> None: '''Метод вызывающий обработку токенов, выдаваемых парсером в зависимости от их типа.''' if start_section is not None: @@ -210,7 +213,7 @@ class NamespaceIniFiller: elif error is not None: self._set_error(error[0], 'SyntaxError', error[1]) - def start_section(self, sections: str, lineno) -> None: + def start_section(self, sections: str, lineno: int) -> None: '''Метод для получения доступа и создания пространств имен.''' if self.restricted: self.modify_only = sections[0] not in self.available_sections @@ -248,7 +251,7 @@ class NamespaceIniFiller: self.current_namespace = self.current_namespace.\ _namespaces[section] - def clear_section(self, sections: list, lineno) -> None: + def clear_section(self, sections: list, lineno: int) -> None: '''Метод для очистки пространства имен.''' if self.restricted: self.modify_only = sections[0] not in self.available_sections @@ -291,7 +294,7 @@ class NamespaceIniFiller: " namespace.".format(current_namespace. get_fullname())) - def start_table(self, sections: str, row, lineno) -> None: + def start_table(self, sections: str, row: dict, lineno: int) -> None: '''Метод для создания и модификации таблиц.''' if self.restricted: self.modify_only = sections[0] not in self.available_sections @@ -335,7 +338,8 @@ class NamespaceIniFiller: table.add_row(row) table_variable.source = table - def define_key(self, key: str, value: str, optype, lineno) -> None: + def define_key(self, key: str, value: str, optype: Define, + lineno: int) -> None: '''Метод для создания и модификации переменных.''' if self.current_namespace is None: return @@ -360,7 +364,7 @@ class NamespaceIniFiller: else: self.remove_value(key, value, lineno) - def change_value(self, key: str, value: str, lineno) -> None: + def change_value(self, key: str, value: str, lineno: int) -> None: '''Метод для изменения значения переменной.''' variable = self.current_namespace[key] if variable.readonly: @@ -370,7 +374,7 @@ class NamespaceIniFiller: return variable.source = value - def define_variable(self, key: str, value: str, lineno) -> None: + def define_variable(self, key: str, value: str, lineno: int) -> None: '''Метод для создания переменных в calculate.ini файле.''' if not self.modify_only: VariableNode(key, self.current_namespace, variable_type=IniType, @@ -382,7 +386,7 @@ class NamespaceIniFiller: self.current_namespace.get_fullname(), key)) - def append_value(self, key: str, value: str, lineno) -> None: + def append_value(self, key: str, value: str, lineno: int) -> None: '''Метод выполняющий действия возложенные на оператор +=.''' variable = self.current_namespace[key] if variable.readonly: @@ -416,7 +420,7 @@ class NamespaceIniFiller: variable.source = variable_value - def remove_value(self, key: str, value: str, lineno) -> None: + def remove_value(self, key: str, value: str, lineno: int) -> None: '''Метод выполняющий действия возложенные на оператор -=.''' variable = self.current_namespace[key] if variable.readonly: @@ -450,7 +454,8 @@ class NamespaceIniFiller: variable.source = variable_value - def update_hash(self, key: str, value: str, optype, lineno): + def update_hash(self, key: str, value: str, optype: Define, + lineno: int) -> None: '''Метод для изменения переменных хэшей через calculate.ini.''' if self.current_namespace.readonly: self._set_error(lineno, 'VariableError', @@ -496,12 +501,12 @@ class NamespaceIniFiller: self.current_namespace.source = hash_to_update - def _set_error(self, lineno, error_type, line): + def _set_error(self, lineno: int, error_type: str, line: str) -> None: '''Метод для добавления ошибки в лог.''' self._errors.append("{}:{}: {}".format(error_type, lineno, line)) @property - def errors(self): + def errors(self) -> List[str]: errors = self._errors self._errors = [] return errors @@ -511,7 +516,8 @@ class VariableLoader: '''Класс загрузчика переменных из python-файлов и из ini-файлов.''' ini_basename = "calculate.ini" - def __init__(self, datavars, variables_path, repository_map=None): + def __init__(self, datavars: "Datavars", variables_path: str, + repository_map: Optional[Dict[str, str]] = None): self.datavars = datavars self.logger = datavars.logger self.ini_filler = NamespaceIniFiller() @@ -533,7 +539,7 @@ class VariableLoader: self.datavars.root.add_namespace(package_namespace) self._fill_from_package(package_namespace, directory_path, package) - def load_from_profile(self): + def load_from_profile(self) -> None: '''Метод для загрузки переменных из calculate.ini профиля.''' # Проверяем наличие таблицы репозиториев в переменных. if self.repository_map == {}: @@ -566,7 +572,7 @@ class VariableLoader: profile_path)) self._fill_from_profile_ini(profile_path) - def load_user_variables(self): + def load_user_variables(self) -> None: '''Метод для загрузки переменных из calculate.ini указанных в переменных env_order и env_path.''' try: @@ -625,7 +631,7 @@ class VariableLoader: '{}.{}'.format(package, directory_node.name)) - def _fill_from_profile_ini(self, profile_path): + def _fill_from_profile_ini(self, profile_path: str) -> None: '''Метод для зaполнения переменных из ini-файла.''' profile_walker = ProfileWalker(self.ini_basename, self.repository_map) @@ -637,13 +643,13 @@ class VariableLoader: self.logger.error("Can not load profile variables from" " unexisting file: {}".format(file_path)) - def _get_repository_map(self, datavars): + def _get_repository_map(self, datavars: "Datavars") -> Dict[str, str]: '''Метод для получения из переменной словаря с репозиториями и путями к ним.''' return {repo['name']: repo['path'] for repo in datavars.os.gentoo.repositories} - def fill_from_custom_ini(self, file_path: str): + def fill_from_custom_ini(self, file_path: str) -> None: '''Метод для заполнения переменных из конкретного указанного файла.''' if os.path.exists(file_path): ini_file_text = read_file(file_path) @@ -661,7 +667,7 @@ class VariableLoader: " not exist.".format(file_path)) @contextmanager - def test(self, file_name, namespace): + def test(self, file_name: str, namespace: NamespaceNode): '''Контекстный менеджер для тестирования.''' print('IMPORT: {}.{}'.format(namespace.get_fullname(), file_name)) try: @@ -673,7 +679,7 @@ class VariableLoader: class CalculateIniSaver: '''Класс для сохранения значений переменных в указанные ini-файлы.''' - def __init__(self, ini_parser=None): + def __init__(self): self.ini_parser = CalculateIniParser() self.operations = {Define.assign: '=', Define.append: '+=', @@ -683,8 +689,10 @@ class CalculateIniSaver: environment = Environment(loader=file_loader) self.ini_template = environment.get_template('ini_template') - def save_to_ini(self, target_path, variables_to_save): - '''Метод для сохранения переменных в указанный ini-файл.''' + def save_to_ini(self, target_path: str, + variables_to_save: Dict[str, Dict[str, Any]]) -> None: + '''Метод для сохранения значений переменных, переданных в словаре, в + указанный ini-файл.''' ini_file_text = read_file(target_path) ini_dictionary = self._parse_ini(ini_file_text) @@ -698,7 +706,7 @@ class CalculateIniSaver: with open(target_path, 'w') as ini_file: ini_file.write(ini_file_text) - def _parse_ini(self, ini_file_text): + def _parse_ini(self, ini_file_text: str) -> Dict[str, Dict[str, str]]: '''Метод для разбора текста ini-файла в словарь, в который далее будут добавляться измененные переменные.''' current_namespace = None @@ -725,7 +733,7 @@ class CalculateIniSaver: line_content[1])}) return ini_dictionary - def _get_ini_text(self, ini_dictionary): + def _get_ini_text(self, ini_dictionary: Dict[str, Dict[str, Any]]) -> str: '''Метод для получения текста ini файла, полученного в результате наложения изменений из тегов save в шаблонах.''' ini_text = self.ini_template.render(ini_dictionary=ini_dictionary) @@ -734,9 +742,10 @@ class CalculateIniSaver: class Datavars: '''Класс для хранения переменных и управления ими.''' - def __init__(self, variables_path='calculate/vars', repository_map=None, - logger=None): - self._variables_path = variables_path + def __init__(self, variables_path: str = 'calculate/vars', + repository_map: Optional[Dict[str, str]] = None, + logger: Optional[logging.Logger] = None): + self._variables_path: str = variables_path self._available_packages = self._get_available_packages() if logger is not None: self.logger = logger @@ -765,15 +774,16 @@ class Datavars: except VariableNotFoundError: self.variables_to_save = dict() - def reset(self): + def reset(self) -> None: '''Метод для сброса модуля переменных.''' self.root.clear() self.root = NamespaceNode('') self._available_packages.clear() - self._available_packages = self._get_available_packages() + self._available_packages: Dict[str, + str] = self._get_available_packages() Namespace.set_datavars(self) - def _get_available_packages(self) -> dict: + def _get_available_packages(self) -> Dict[str, str]: '''Метод для получения словаря с имеющимися пакетами переменных и путями к ним.''' variables_path = os.path.join( @@ -788,7 +798,7 @@ class Datavars: available_packages.update({file_name: file_path}) return available_packages - def _load_package(self, package_name): + def _load_package(self, package_name: str) -> None: '''Метод для загрузки переменных содержащихся в указанном пакете.''' self.logger.info("Loading datavars package '{}'".format(package_name)) try: @@ -798,12 +808,12 @@ class Datavars: format(error)) @property - def available_packages(self): + def available_packages(self) -> set: packages = set(self._available_packages) packages.update({'custom'}) return packages - def __getattr__(self, package_name: str): + def __getattr__(self, package_name: str) -> NamespaceNode: '''Метод возвращает ноду пространства имен, соответствующего искомому пакету.''' if package_name in self.root._namespaces: @@ -819,7 +829,7 @@ class Datavars: self._load_package(package_name) return self.root[package_name] - def __getitem__(self, package_name: str) -> None: + def __getitem__(self, package_name: str) -> NamespaceNode: '''Метод возвращает ноду пространства имен, соответствующего искомому пакету.''' if package_name in self.root: @@ -838,7 +848,7 @@ class Datavars: self._load_package(package_name) return self.root[package_name] - def __contains__(self, package_name): + def __contains__(self, package_name: str) -> bool: if package_name in self.root._namespaces: return True elif package_name == 'custom': @@ -855,10 +865,10 @@ class Datavars: self._load_package(package_name) return True - def add_namespace(self, namespace_node): + def add_namespace(self, namespace_node: NamespaceNode) -> None: self.root.add_namespace(namespace_node) - def create_tasks(self): + def create_tasks(self) -> None: '''Метод для создания всех необходимых пространств имен для работы задач.''' tasks = NamespaceNode('tasks') @@ -870,10 +880,10 @@ class Datavars: env.add_namespace('loop') @property - def _namespaces(self): + def _namespaces(self) -> NamespaceNode: return self.root._namespaces - def save_variables(self): + def save_variables(self) -> None: '''Метод для сохранения значений переменных в calculate.ini файлах.''' target_paths = self.main.cl.system.env_path saver = CalculateIniSaver() diff --git a/calculate/vars/main/__init__.py b/calculate/vars/main/__init__.py index a7eae56..f91939b 100644 --- a/calculate/vars/main/__init__.py +++ b/calculate/vars/main/__init__.py @@ -16,7 +16,8 @@ Variable('cl_root_path', type=StringType.readonly, source='/') Variable('cl_template_path', type=ListType.readonly, source=['./tests/templates/testfiles/test_runner']) -Variable('cl_ignore_files', type=ListType.readonly, source=['*.swp']) +Variable('cl_ignore_files', type=ListType.readonly, + source=['*.swp', '.git']) Variable('cl_config_path', type=StringType.readonly, source='/var/lib/calculate/config') @@ -30,8 +31,5 @@ Variable('cl_exec_dir_path', type=StringType.readonly, Variable('cl_resolutions', type=ListType.readonly, source=["1680x1050", "1024x768"]) -Variable('cl_resolutions', type=ListType.readonly, - source=["1680x1050", "1024x768"]) - Variable('cl_image_formats', type=ListType.readonly, source=["GIF", "JPG", "JPEG", "PNG", "GFXBOOT"]) diff --git a/calculate/vars/main/cl/__init__.py b/calculate/vars/main/cl/__init__.py index 4f26ff3..6f7cdbf 100644 --- a/calculate/vars/main/cl/__init__.py +++ b/calculate/vars/main/cl/__init__.py @@ -107,6 +107,7 @@ def get_isoscan_fullpath(base_path, filename): def import_variables(): Variable("chroot_path", type=StringType, source=Calculate(lambda x: x.value, "main.cl_chroot_path")) + Variable("root_path", type=StringType, source=Calculate(lambda x: x.value, "main.cl_root_path")) diff --git a/calculate/vars/main/os/__init__.py b/calculate/vars/main/os/__init__.py index 6dc6ffa..e264409 100644 --- a/calculate/vars/main/os/__init__.py +++ b/calculate/vars/main/os/__init__.py @@ -92,16 +92,21 @@ def import_variables(): Variable("shortname", type=StringType, source=Calculate(detect_other_shortname, "main.cl.chroot_path")) + Variable("name", type=StringType, source=Calculate(dict_value, dictLinuxName, ".shortname", Static("Linux"))) + Variable("subname", type=StringType, source=Calculate(dict_value, dictLinuxSubName, ".shortname", Static(""))) + Variable("system", type=StringType, source=Calculate(dict_value, dictNameSystem, ".shortname", Static(""))) + Variable("ver", type=StringType, source=Calculate(get_linux_version, "main.cl.chroot_path", "main.os.gentoo.make_profile")) + Variable("build", type=StringType, source="") diff --git a/output b/output deleted file mode 100644 index 04c9bc3..0000000 --- a/output +++ /dev/null @@ -1,1339 +0,0 @@ -============================= test session starts ============================== -platform linux -- Python 3.9.0, pytest-6.2.1, py-1.10.0, pluggy-0.13.1 -rootdir: /var/calculate/development/calculate-utils-4-lib, configfile: pytest.ini -collected 770 items - -tests/commands/test_commands.py .... -tests/parameters/test_parameters.py .........VALIDATED: test-1 -VALIDATED: test-2 -VALIDATED: test-3 -........VALUE: -VALUE: -.VALUE: -VALUE: -VALUE: -VALUE: -.VALUE: -VALUE: -.VALUE: -VALUE: -VALUE: -.VALUE: -VALUE: -.VALUE: -... -tests/scripts/test_scripts.py ..............................................CREATE COMMON PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/scripts/testfiles/templates/action_1[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/scripts/testfiles/templates/action_1/dir_0[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/scripts/testfiles/templates/action_1/dir_0/file_0[ ok ] - * Action parameter value 'action_2' does not match its current value 'action_1'. Template: /var/calculate/development/calculate-utils-4-lib/tests/scripts/testfiles/templates/action_2 -. * Action parameter value 'action_1' does not match its current value 'action_2'. Template: /var/calculate/development/calculate-utils-4-lib/tests/scripts/testfiles/templates/action_1 -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/scripts/testfiles/templates/action_2[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/scripts/testfiles/templates/action_2/dir_1[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/scripts/testfiles/templates/action_2/dir_1/file_0[ ok ] -.. -tests/templates/test_directory_processor.py .CREATE COMMON PACKAGE: test-category/other-package-1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_0/root[ ok ] -.CREATE COMMON PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_1/root[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_1/root/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_2/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_2/root/dir_1[ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_4/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_4/root/dir_3[ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_3/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_3/root/dir_2[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_3/root/dir_2/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_5/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_5/root/dir_4[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_5/root/dir_4/file_0[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_5/root/dir_5[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_5/root/dir_5/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.CREATE COMMON PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_6/root[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_6/root/file_1[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 -.RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_7/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_7/root/dir_6[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_7/root/dir_6/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 -.RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_8/root[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_8/root/file_2[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 -.RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_9/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_9/root/etc[ ok ] - * archive file is missed, target file was used instead. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_9/root/etc/file_3 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_9/root/etc/file_3[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 -.RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_10/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_10/root/etc[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_10/root/etc/file_4[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_10/root/etc/file_5[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_10/root/etc/dir_7[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_10/root/etc/dir_7/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_11/root[ ok ] - * Action parameter is not set for template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_11/root/dir_8 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_12/root_0[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_12/root_0/etc[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_12/root_0/etc/file_6[ ok ] - * Action parameter value 'update' does not match its current value 'install'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_12/root_1 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_13/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_13/root/etc[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_13/root/etc/dir_9[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_13/root/etc/dir_9/file_8[ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_14/root[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_14/root/dir_10[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_14/root/dir_10/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/other-package-1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_14/root/dir_9[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_14/root/dir_9/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -RETURN CREATED PACKAGE: test-category/other-package-1.1 -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_15/root[ ok ] -MESSAGES: -success -> Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_15/root -error -> Template execution error: collision: 'package' parameter is not defined for template with 'append' parameter. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_15/root/dir_11 -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_16/root[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_16/root/dir_12[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_16/root/dir_12/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 -.RETURN CREATED PACKAGE: test-category/other-package-1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_17/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_17/root/dir_13[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_17/root/dir_13/file_0[ ok ] - * 'package' parameter value 'test-category/test-package-1.0' does not match its current target package 'test-category/other-package-1.1'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_17/root/dir_14 - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -RETURN CREATED PACKAGE: test-category/other-package-1.1 -.RETURN CREATED PACKAGE: test-category/other-package-1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_18/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_18/root/dir_15[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_18/root/dir_15/file_0[ ok ] - * 'package' parameter value 'test-category/test-package-1.0' does not match its current target package 'test-category/other-package-1.1'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_18/root/dir_16 - * Processing packages from merge parameter... -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_18/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_18/root/dir_16[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_18/root/dir_16/file_0[ ok ] - * All packages are merged. [ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -RETURN CREATED PACKAGE: test-category/other-package-1.1 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_19/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_19/root/etc[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_19/root/etc/dir_17[ ok ] - * archive file is missed, target file was used instead. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_19/root/etc/dir_17/file_0 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_19/root/etc/dir_17/file_0[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_19/root/etc/dir_17/dir_0[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_19/root/etc/dir_17/dir_0/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_20/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_20/root/etc[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_20/root/etc/dir_19[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_20/root/etc/dir_19/file_0[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_20/root/etc/dir_19/dir_0[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_20/root/etc/dir_19/dir_0/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/other-package-1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_22[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_22/dir_21[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_22/dir_21/file_0[ ok ] - * 'package' parameter value 'test-category/test-package-1.0' does not match its current target package 'test-category/other-package-1.1'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_22/dir_22 - * 'package' parameter value 'test-category/new-package-0.1.1' does not match its current target package 'test-category/other-package-1.1'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_23 - * Processing packages from merge parameter... -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_22[ ok ] - * 'package' parameter value 'test-category/other-package-1.1' does not match its current target package 'test-category/test-package-1.0'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_22/dir_21 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_22/dir_22[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_22/dir_22/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_23[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_21/root/dir_23/file_0[ ok ] - * All packages are merged. [ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -RETURN CREATED PACKAGE: test-category/other-package-1.1 -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 -.RETURN CREATED PACKAGE: test-category/other-package-1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_24[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_24/dir_24[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_24/dir_24/file_0[ ok ] - * 'package' parameter value 'test-category/test-package-1.0' does not match its current target package 'test-category/other-package-1.1'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_24/dir_25 - * 'package' parameter value 'test-category/new-package-0.1.1' does not match its current target package 'test-category/other-package-1.1'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_26 - * Processing packages from merge parameter... -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_26[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_26/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_24[ ok ] - * 'package' parameter value 'test-category/other-package-1.1' does not match its current target package 'test-category/test-package-1.0'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_24/dir_24 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_24/dir_25[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_22/root/dir_24/dir_25/file_0[ ok ] - * All packages are merged. [ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -RETURN CREATED PACKAGE: test-category/other-package-1.1 -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 -.RETURN CREATED PACKAGE: test-category/other-package-1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root[ ok ] - * 'package' parameter value 'test-category/test-package-1.0' does not match its current target package 'test-category/other-package-1.1'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/file_9 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/dir_27[ ok ] - * 'package' parameter value 'test-category/test-package-1.0' does not match its current target package 'test-category/other-package-1.1'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/dir_27/dir_27 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/dir_27/dir_28[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/dir_27/dir_28/file_0[ ok ] - * Processing packages from merge parameter... -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/file_9[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/dir_27[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/dir_27/dir_27[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/dir_27/dir_27/file_0[ ok ] - * 'package' parameter value 'test-category/other-package-1.1' does not match its current target package 'test-category/test-package-1.0'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_23/root/dir_27/dir_28 - * All packages are merged. [ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -RETURN CREATED PACKAGE: test-category/other-package-1.1 -DIRECTORY TREE: -test-category/test-package-1.0 -> -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_24/root[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_24/root/file_10[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_25/root[ ok ] - * Condition is failed. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_25/root/file_11 -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_26/root[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_26/root/dir_29[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_26/root/dir_29/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_27/root[ ok ] - * Error during handling condition: 'variables' is undefined. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_27/root/dir_30 -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_28/root[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_28/root/file_12[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_28/root[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_28/root/file_12[ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_29/root[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_29/root/file_13[ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_30/root[ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_31/root[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_31/root/file_15[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_31/root/file_16[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_31/root/file_17[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_31/root/dir_31[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_31/root/dir_31/file_0[ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_32/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_32/install/dir_32[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_32/install/dir_32/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_32/update[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_32/update/dir_33[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_32/update/dir_33/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_33/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_33/install/dir_34[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_33/update[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_33/update/dir_35[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_33/update/dir_35/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/dir_37[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/dir_37/file_0[ ok ] - * Processing packages from merge parameter... - * All packages are merged. [ ok ] - * Processing handlers... - * stdout from template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/handler_0: -CWD: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/etc - - - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/handler_0[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/handler_1[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/handler_2[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/dir_37/handler_3[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/handler_4[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/handler_5[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/handler_5/file_0[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_35/install/dir_37/handler_9[ ok ] - * Handler 'handler_6' is not found. - * Handler 'handler_7' is not found. -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install[ ok ] - * stdout from template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install/script_0: -IN script_0 -current dir: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/etc/dir_50 - - - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install/script_0[ ok ] - * 'package' parameter value 'test-category/other-package-1.1' does not match its current target package 'test-category/test-package-1.0'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install/script_1 - * 'package' parameter value 'test-category/new-package-0.1.1' does not match its current target package 'test-category/test-package-1.0'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install/script_2 - * Processing packages from merge parameter... -RETURN CREATED PACKAGE: test-category/other-package-1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install[ ok ] - * stdout from template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install/script_1: -script_1 creates file: ./file_1 - - - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install/script_1[ ok ] -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install[ ok ] - * stdout from template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install/script_2: -script_2 creates file: ./file_2 - - - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_36/install/script_2[ ok ] - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_51[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_51/file_0[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_52[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_52/script[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_53[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_53/script[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_54[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_54/dir_1[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_54/dir_1/script[ ok ] - * package 'test-category/unmerged-package' does not match the template condition. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_37/install/dir_54/dir_0 - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_38/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_38/install/dir_55[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_38/install/dir_55/file_0[ ok ] - * 'package' parameter value 'test-category/other-package-1.1' does not match its current target package 'test-category/test-package-1.0'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_38/install/dir_56 - * Processing packages from merge parameter... -RETURN CREATED PACKAGE: test-category/other-package-1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_38/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_38/install/dir_56[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_38/install/dir_56/dir_0[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_38/install/dir_56/dir_0/file_0[ ok ] - * package 'test-category/other-package' does not match the template condition. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_38/install/dir_56/dir_1 - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_39/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_39/install/dir_57[ ok ] -CREATE COMMON PACKAGE: test-category/test-package-0.9.6-r1 -REMOVE /etc/dir_57/file_0 FROM PACKAGE test-category/test-package-0.9.6-r1 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_39/install/dir_57/file_0[ ok ] - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -RETURN CREATED PACKAGE: test-category/test-package-0.9.6-r1 -RETURN CREATED PACKAGE: test-category/test-package-1.0 -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_40/install[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_40/install/dir_58[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-0.9.6-r1 -REMOVE /etc/dir_58/file_0 FROM PACKAGE test-category/test-package-0.9.6-r1 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_40/install/dir_58/file_0[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-0.9.6-r1 -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_41/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_41/install/dir_59[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-0.9.6-r1 -REMOVE /etc/dir_59/file_0 FROM PACKAGE test-category/test-package-0.9.6-r1 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_41/install/dir_59/file_0[ ok ] - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -RETURN CREATED PACKAGE: test-category/test-package-0.9.6-r1 -RETURN CREATED PACKAGE: test-category/test-package-1.0 -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_42/install[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_42/install/template[ ok ] - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_43/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_43/install/dir_60[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_43/install/dir_60/file_0[ ok ] - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_44/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_44/install/dir_61[ ok ] - * archive file is missed, target file was used instead. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_44/install/dir_61/file_0 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_44/install/dir_61/file_0[ ok ] - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_45/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_45/install/dir_62[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_45/install/dir_62/file_0[ ok ] - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Action parameter is not set for template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_46/file_to_ignore - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_46/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_46/install/dir_63[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_46/install/dir_63/file_0[ ok ] - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Action parameter value 'install' does not match its current value 'update'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_47/install - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_47/not_install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_47/not_install/dir_65[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_47/not_install/dir_65/file_0[ ok ] - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_48/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_48/install/dir_66[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_48/install/dir_66/file_0[ ok ] - * Processing packages from merge parameter... - * Warning: package 'test-category/new-package-0.1.1' not found for action 'install'. - * All packages are merged. [ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_49/merge[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_49/merge/dir_67[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_49/merge/dir_67/script[ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_50/unmerge[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_50/unmerge/dir_68[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_50/unmerge/dir_68/script[ ok ] - * package 'test-category/build-package' does not match the template condition. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_50/unmerge/dir_69 -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_51/merge[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_51/merge/dir_70[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_51/merge/dir_70/script[ ok ] - * package 'test-category/build-package' does not match the template condition. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_51/merge/dir_71 - * 'package' parameter value 'test-category/test-package-1.0' does not match its current target package 'None'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_51/merge/dir_72 - * Processing packages from merge parameter... - * All packages are merged. [ ok ] -.RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_52/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_52/install/dir_73[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_52/install/dir_73/file_0[ ok ] - * 'package' parameter value 'test-category/new-package-0.1.1' does not match its current target package 'test-category/test-package-1.0'. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_52/install/dir_74 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_52/install/dir_75[ ok ] - * Processing packages from merge parameter... -RETURN CREATED PACKAGE: test-category/new-package-0.1.1 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_52/install[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_52/install/dir_74[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_52/install/dir_74/file_0[ ok ] - * All packages are merged. [ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_53/install[ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_54/install[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_54/install/link_1[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_54/install/link_0[ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_55/install[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_55/install/dir[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_55/install/dir/file_1[ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_56/install[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_56/install/file_19[ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_57/install[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * archive file is missed, target file was used instead. Template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_57/install/file_20 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_57/install/file_20[ ok ] -. * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_58/install[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_58/install/dir_76[ ok ] - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_58/install/dir_76/file_0[ ok ] -...RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_61/file_22[ ok ] - * Processed directory: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_61/install[ ok ] -RETURN CREATED PACKAGE: test-category/test-package-1.0 - * Processed template: /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_dir_processor_root/templates_61/install/file_21[ ok ] -.etc/ - dir_12/ - file_0 - dir_17/ - dir_0/ - file_0 - dir_18/ - dir_0/ - file_0 - dir_20/ - file_0 - dir_6/ - file_0 - dir_7/ - file_0 - file_1 - file_12 - file_2 - file_4 - group - passwd - dir_50/ - test - file_0 - file_1 - file_2 - dir_57/ - file_0 - dir_58/ - file_0 - dir_59/ - file_0 - img.gif - dir_60/ - file_0 - dir_61/ - file_0 - ._cfg0000_file_0 - file_18 - dir_75/ - file_0 - file_1 - link_3/ - file_0 - file_1 - file_19 - file_20 - dir_76/ - file_0 - dir_77/ - file_0 - dir_78/ - file_0 - file_1 - file_0 - dir_1/ - dir_3/ - dir_2/ - file_0 - dir_4/ - file_0 - dir_5/ - dir_6/ - file_0 - ._cfg0000_file_2 - ._cfg0000_file_3 - file_5 - file_6 - file_8 - dir_10/ - file_0 - dir_9/ - file_0 - dir_13/ - file_0 - dir_15/ - file_0 - dir_16/ - file_0 - dir_19/ - dir_0/ - file_0 - dir_21/ - file_0 - dir_22/ - file_0 - dir_23/ - file_0 - dir_24/ - file_0 - dir_26/ - file_0 - dir_25/ - file_0 - dir_28/ - file_0 - file_9 - dir_27/ - file_0 - file_10 - dir_29/ - file_0 - ._cfg0000_file_12 - file_13 - file_17 - dir_31/ - file_0 - file_15 - file_16 - dir_32/ - file_0 - dir_33/ - file_0 - dir_35/ - file_0 - dir_37/ - file_0 - dir_38/ - file_0 - dir_39/ - file_0 - dir_40/ - file_0 - dir_41/ - file_0 - dir_43/ - file_0 - dir_48/ - file_0 - dir_42/ - file_0 - dir_51/ - file_0 - dir_52/ - file_0 - dir_53/ - file_0 - dir_54/ - file_0 - dir_55/ - file_0 - dir_56/ - file_0 - copy.gif - dir_62/ - file_0 - dir_63/ - file_0 - dir_65/ - file_0 - dir_66/ - file_0 - dir_67/ - file_0 - dir_68/ - file_0 - dir_70/ - file_0 - dir_73/ - file_0 - dir_74/ - file_0 - patch/ - link_1 - link_0 - ._cfg0000_file_20 - file_22/ - file_22 - file_21 -.. -tests/templates/test_parameters_processor.py ................. -tests/templates/test_template_engine.py ............................ -tests/templates/test_template_executor.py ......File exists -.........................CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.........................CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -START EXECUTION -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -input path = /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_executor_root/var/lib/calculate/config-archive/etc/append_join_file_testfiles/file_10 -Output: -#------------------------------------------------------------------------------- -# Modified by Calculate Utilities 4.0 -# Processed template files: -# /path/to/template -#------------------------------------------------------------------------------- -section-name { - parameter-1 yes; - parameter-4 yes; -}; - -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -output_path = /var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_executor_root/etc/append_join_file_testfiles/._cfg0003_file_11 -Output: -#------------------------------------------------------------------------------- -# Modified by Calculate Utilities 4.0 -# Processed template files: -# /path/to/template -#------------------------------------------------------------------------------- -section-name { - parameter-1 yes; - parameter-4 yes; -}; - -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -output text: -#------------------------------------------------------------------------------- -# Modified by Calculate Utilities 4.0 -# Processed template files: -# /path/to/template -#------------------------------------------------------------------------------- -section-1 { - parameter-1 no; -}; - -section-2 { - parameter-1 yes; - parameter-2 10; -}; - - -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -output text: -#------------------------------------------------------------------------------- -# Modified by Calculate Utilities 4.0 -# Processed template files: -# /path/to/template -#------------------------------------------------------------------------------- -section-1 { - parameter-1 no; -}; - -section-2 { - parameter-1 10; -}; - - -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -......................CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.. -tests/templates/test_template_filters.py ......... -tests/templates/test_template_wrapper.py CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -...CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -...CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -...CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.....CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -..CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -..CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -PROTECTED SET -{'/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/etc', '/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/usr/share/gnupg/qualified.txt'} -UNPROTECTED SET -{'/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/etc/dconf', '/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/etc/revdep-rebuild', '/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/etc/gentoo-release', '/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/etc/ca-certificates.conf', '/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/etc/fonts/fonts.conf', '/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/etc/sandbox.d', '/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/etc/terminfo'} -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/etc -/var/calculate/development/calculate-utils-4-lib/tests/templates/testfiles/test_wrapper_root/usr/share/gnupg/qualified.txt -DEL: test-category/other-package-1.1 -...CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -... -tests/templates/format/test_base.py .......... -tests/templates/format/test_bind.py .DEL: test-category/test-package-1.0 -.....result text: -section-name-1 { - parameter-name /homeless/poorness; - one-more-parameter no; -}; - -// Comment1 -section-name-2 { - parameter-name /home/divanov/Home; - // Comment2 - // Comment3 - other-parameter yes; -}; - -section-name-3 { - // Comment - another-parameter 1; - unspoken-parameter 1; -}; - - -../* - * Refer to the named.conf(5) and named(8) man pages, and the documentation - * in /usr/share/doc/bind-* for more details. - * Online versions of the documentation can be found here: - * https://kb.isc.org/article/AA-01031 - * - * If you are going to set up an authoritative server, make sure you - * understand the hairy details of how DNS works. Even with simple mistakes, - * you can break connectivity for affected parties, or cause huge amounts of - * useless Internet traffic. - */ - -/* - * You might put in here some ips which are allowed to use the cache or - * recursive queries - */ -acl "trusted" { - 127.0.0.0/8; - 10.0.0.0/8; - 192.168.1.0/24; - ::1/128; -}; - -acl "dns_servers" { - 127.0.0.1; - 10.0.1.3; - 10.1.0.3; - 10.2.0.3; - 10.4.0.3; -}; - -options { - directory "/var/bind"; - pid-file "/run/named/named.pid"; - disable-empty-zone "10.in-addr.arpa"; - - /* https://www.isc.org/solutions/dlv >=bind-9.7.x only */ - //bindkeys-file "/etc/bind/bind.keys"; - - listen-on-v6 { ::1; }; - listen-on { 10.0.0.0/8; 127.0.0.1; }; - - allow-query { - /* - * Accept queries from our "trusted" ACL. We will - * allow anyone to query our master zones below. - * This prevents us from becoming a free DNS server - * to the masses. - */ - trusted; - }; - - allow-query-cache { - /* Use the cache for the "trusted" ACL. */ - trusted; - }; - - allow-recursion { - /* Only trusted addresses are allowed to use recursion. */ - trusted; - }; - - recursion yes; - - response-policy { - zone "rpz.zone"; - }; -}; - -controls { - // local host -- default key. - inet 127.0.0.1 port 953 -allow { 127.0.0.1; "rndc-users"; } - keys { "rndc-remote"; "rndc-key"; }; - inet 127.0.0.1 allow { localhost; }; -}; - -include "/etc/bind/rndc.key"; - -zone "." in { - type hint; - file "/var/bind/named.cache"; -}; - -zone "localhost" IN { - type master; - file "pri/localhost.zone"; - notify no; -}; - -zone "rpz.zone" { - type master; - file "/var/bind/pri/rpz.zone"; - allow-query {trusted;}; - allow-update {none;}; -}; - -# DMZ -zone "dmz.calculate.ru" IN { - type slave; - file "/var/bind/sec/dmz.calculate.ru.zone"; - masters { 10.1.0.3; }; -}; - -zone "1.10.in-addr.arpa" IN { - type slave; - file "/var/bind/sec/dmz.calculate.ru.rev.zone"; - masters { 10.1.0.3; }; -}; - -..DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -... -tests/templates/format/test_compiz.py ............ -tests/templates/format/test_contents.py .CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -REMOVE /etc/dir_0/file_1 FROM PACKAGE test-category/test-package-1.0 -REMOVE /etc/dir_1/file_0 FROM PACKAGE test-category/test-package-1.0 -REMOVE /etc/dir_1/file_1 FROM PACKAGE test-category/test-package-1.0 -REMOVE /etc/dir_2 FROM PACKAGE test-category/test-package-1.0 -REMOVE /etc/dir_3/link_0 FROM PACKAGE test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -REMOVE /etc/dir_0/file_0 FROM PACKAGE test-category/test-package-1.0 -REMOVE /etc/dir_0/file_1 FROM PACKAGE test-category/other-package-1.1 -REMOVE /etc/dir_1/file_1 FROM PACKAGE test-category/test-package-1.0 -REMOVE /etc/dir_1/file_3 FROM PACKAGE test-category/test-package-1.0 -REMOVE /etc/dir_1/file_0 FROM PACKAGE test-category/other-package-1.1 -REMOVE /etc/dir_1/file_2 FROM PACKAGE test-category/other-package-1.1 -REMOVE /etc/dir_3 FROM PACKAGE test-category/test-package-1.0 -REMOVE /etc/dir_4 FROM PACKAGE test-category/other-package-1.1 -REMOVE /etc/dir_5/link_0 FROM PACKAGE test-category/test-package-1.0 -REMOVE /etc/dir_5/link_1 FROM PACKAGE test-category/other-package-1.1 -DEL: test-category/test-package-1.0 -DEL: test-category/other-package-1.1 -CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -CREATE ISOLATED PACKAGE: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -DEL: test-category/other-package-1.1 -DEL: test-category/test-package-1.0 -.. -tests/templates/format/test_dovecot.py ............ -tests/templates/format/test_json.py .... -tests/templates/format/test_kde.py .............ORIGINAL DICT: -OrderedDict([(('', 'Test'), - OrderedDict([(('', 'Name[pl]'), - ['Przechowalnia certyfikatów i\xa0kluczy']), - (('', 'Comment[fr]'), - ['Trousseau de clés de GNOME\xa0: composant ' - 'PKCS#11']), - (('', 'NoShow'), ['true'])]))]) -.... -tests/templates/format/test_kernel.py .......... -tests/templates/format/test_ldap.py ............#------------------------------------------------------------------------------- -# Modified by Calculate Utilities 4.0 -# Processed template files: -# /path/to/ancient/template -# /path/to/template -#------------------------------------------------------------------------------- -loglevel 0 -allow bind_v2 -modulepath /usr/lib/openldap/openldap - -database bdb -checkpoint 1024 5 - - -. -tests/templates/format/test_openrc.py ............ -tests/templates/format/test_patch.py .... -tests/templates/format/test_postfix.py ........... -tests/templates/format/test_procmail.py ........... -tests/templates/format/test_proftpd.py .........ORIGINAL -OrderedDict([(('', '', 'ServerName'), ['"ProFTPD Anonymous Server"']), - (('', '', 'ServerType'), ['standalone']), - (('', (('Anonymous', '~ftp'), ('Limit', 'LOGIN')), 'DisplayLogin'), - ['welcome.msg']), - (('', - (('Anonymous', '~ftp'), ('Limit', 'LOGIN')), - 'DisplayFirstChdir'), - ['.message']), - (('', (('Anonymous', '~ftp'), ('Limit', 'WRITE')), 'Order'), - ['allow,deny']), - (('', - (('Anonymous', '~ftp'), ('Limit', 'WRITE')), - 'Allow from', - '10.0.0'), - ['']), - (('', - (('Anonymous', '~ftp'), ('Limit', 'WRITE')), - 'Deny from', - 'all'), - [''])]) -TEMPLATE -OrderedDict([(('!', '', 'ServerType'), ['standalone']), - (('!', (('Anonymous', '~ftp'), ('Limit', 'WRITE')), 'Order'), - ['allow,deny']), - (('!', - (('Anonymous', '~ftp'), ('Limit', 'WRITE')), - 'Allow from', - '10.0.0'), - ['']), - (('!', - (('Anonymous', '~ftp'), ('Limit', 'WRITE')), - 'Deny from', - 'all'), - [''])]) -. -tests/templates/format/test_raw.py ......string: #!/bin/bash -echo "message_1" -groupdict: {'shebang': '#!/bin/bash\n'} -.string: #!/bin/bash -#------------------------------------------------------------------------------- -# Modified by Calculate Utilities 4.0 -# Processed template files: -# path/to/ancient/template -#------------------------------------------------------------------------------- -echo "message_1" -groupdict: {'shebang': '#!/bin/bash\n'} -. -tests/templates/format/test_regex.py ........... -tests/templates/format/test_samba.py .............. -tests/templates/format/test_xml_gconf.py ... - - - - - - - Something I can never have. - - - True if the command used to handle this type of URL should be run in a terminal. - - - - - - -. - - - - - - - Something I can never have. - - - True if the command used to handle this type of URL should be run in a terminal. - - - - - - -. - - - - - - - Something I can never have. - - - True if the command used to handle this type of URL should be run in a terminal. - - - - - - -. -tests/templates/format/test_xml_xfce.py ..... -tests/utils/test_files.py .... -tests/utils/test_images.py .........s.......s.. -tests/utils/test_package.py ................DICT: -OrderedDict([('/usr', {'type': 'dir'}), - ('/usr/bin', {'type': 'dir'}), - ('/usr/bin/rview', - {'mtime': '1573538053', 'target': 'vim', 'type': 'sym'}), - ('/usr/bin/rvim', - {'mtime': '1573538053', 'target': 'vim', 'type': 'sym'}), - ('/usr/bin/vim', - {'md5': '30acc0f256e11c1ecdb1bd80b688d238', - 'mtime': '1573538056', - 'type': 'obj'}), - ('/usr/bin/vimdiff', - {'mtime': '1573538053', 'target': 'vim', 'type': 'sym'}), - ('/etc', {'type': 'dir'}), - ('/etc/test_dir_1', {'type': 'dir'})]) -TEXT: -dir /usr -dir /usr/bin -sym /usr/bin/rview -> vim 1573538053 -sym /usr/bin/rvim -> vim 1573538053 -obj /usr/bin/vim 30acc0f256e11c1ecdb1bd80b688d238 1573538056 -sym /usr/bin/vimdiff -> vim 1573538053 -dir /etc -dir /etc/test_dir_1 - -ORIGINAL LEN = 224 -OUTPUT LEN = 224 -DONE -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -ORIGINAL: -b'dir /usr\ndir /usr/bin\nsym /usr/bin/rview -> vim 1573538053\nsym /usr/bin/rvim -> vim 1573538053\nobj /usr/bin/vim 30acc0f256e11c1ecdb1bd80b688d238 1573538056\nsym /usr/bin/vimdiff -> vim 1573538053\ndir /etc\ndir /etc/test_dir_2\nobj /etc/test_dir_2/file_2.cfg a371f4d456d471ac0ed0e8befff1cb6d 1602168237\n' -RESULT: -b'dir /usr\ndir /usr/bin\nsym /usr/bin/rview -> vim 1573538053\nsym /usr/bin/rvim -> vim 1573538053\nobj /usr/bin/vim 30acc0f256e11c1ecdb1bd80b688d238 1573538056\nsym /usr/bin/vimdiff -> vim 1573538053\ndir /etc\ndir /etc/test_dir_2\nobj /etc/test_dir_2/file_2.cfg a371f4d456d471ac0ed0e8befff1cb6d 1602168237\n' -DEL: test-category/test-package-1.0 -.CREATE ISOLATED PACKAGE: test-category/test-package-1.0 -ORIGINAL: -b'dir /usr\ndir /usr/bin\nsym /usr/bin/rview -> vim 1573538053\nsym /usr/bin/rvim -> vim 1573538053\nobj /usr/bin/vim 30acc0f256e11c1ecdb1bd80b688d238 1573538056\nsym /usr/bin/vimdiff -> vim 1573538053\ndir /etc\ndir /etc/test_dir_2\nsym /etc/test_dir_2/symlink -> file_2.cfg 1602168237\n' -RESULT: -b'dir /usr\ndir /usr/bin\nsym /usr/bin/rview -> vim 1573538053\nsym /usr/bin/rvim -> vim 1573538053\nobj /usr/bin/vim 30acc0f256e11c1ecdb1bd80b688d238 1573538056\nsym /usr/bin/vimdiff -> vim 1573538053\ndir /etc\ndir /etc/test_dir_2\nsym /etc/test_dir_2/symlink -> file_2.cfg 1602168237\n' -DEL: test-category/test-package-1.0 -.................. -tests/variables/test_calculateini.py ..... -tests/variables/test_datavars.py ........................................{'name': 'name_1', 'value': 'value_1'} -...........NEW HASH VALUE: {'key_1': 'new_value', 'key_2': 'weird_value'} -........device_list = ['/dev/sda', '/dev/sdb'] -.............logged messages: [('INFO', "Loading datavars package 'os'"), ('INFO', "Load variables from profile: '/var/calculate/development/calculate-utils-4-lib/tests/variables/testfiles/gentoo/repos/distros/profiles/CLD/amd64'."), ('INFO', "Loading datavars package 'main'"), ('INFO', "Loading variables from file: 'system'"), ('ERROR', 'SyntaxError:1: [section'), ('ERROR', 'SyntaxError:2: varval1 = value1'), ('ERROR', 'SyntaxError:3: varval2 = value2'), ('ERROR', "VariableError:4: variables package 'section' is not found."), ('ERROR', 'SyntaxError:6: eee'), ('ERROR', 'SyntaxError:8: [section2'), ('ERROR', "VariableError:10: can not clear namespace 'section'. Variables package 'section' is not found."), ('ERROR', 'SyntaxError:11: [section][][sub]'), ('ERROR', 'SyntaxError:12: [section][][sub][]'), ('WARNING', 'Some variables was not loaded.'), ('INFO', "Variables from 'system' are loaded"), ('INFO', "Loading variables from file: 'local'"), ('WARNING', "File 'local' is not found. Variables are not loaded")] -........ -tests/vars/test_vars.py .................... - -======================== 768 passed, 2 skipped in 8.00s ======================== diff --git a/pytest.ini b/pytest.ini index 2b4e4aa..2f93eda 100644 --- a/pytest.ini +++ b/pytest.ini @@ -46,7 +46,9 @@ markers = scripts: marker for testing of the scripts. commands: marker for testing of the commands. + server: marker for testing of the server. + responses: marker for testing of the responses. chroot: marker for testing running by chroot needs_root: marker for tests that needs root rights. diff --git a/run_server.py b/run_server.py index f9850b8..7b77432 100644 --- a/run_server.py +++ b/run_server.py @@ -1,6 +1,9 @@ -from calculate.server.server import Server +import os +os.environ["TESTING"] = "True" + +import uvicorn +from calculate.server.server import app if __name__ == "__main__": - server = Server() - server.run() + uvicorn.run(app, host="127.0.0.1", port=5000, log_level="info") diff --git a/run_templates.py b/run_templates.py index e89d802..109d633 100644 --- a/run_templates.py +++ b/run_templates.py @@ -1,7 +1,10 @@ #! /usr/bin/python3 import argparse + from calculate.templates.template_processor import DirectoryProcessor + from calculate.variables.loader import Datavars + from calculate.utils.io_module import IOModule from calculate.utils.package import NonePackage from calculate.utils.tools import flat_iterable diff --git a/tests/parameters/test_parameters.py b/tests/parameters/test_parameters.py index beeb6a6..b5ab753 100644 --- a/tests/parameters/test_parameters.py +++ b/tests/parameters/test_parameters.py @@ -181,7 +181,7 @@ class TestParameters: full='Test parameters are needed for tests.'), shortname='t').bind('os.linux.fullname', 'os.linux.shortname') - .to_set('os.linux.subname')) + .set_to('os.linux.subname')) assert PARAMS['test'].value == 'Calculate Linux Desktop -> CLD' datavars.os.linux['fullname'].set('Gentoo Linux') @@ -189,7 +189,7 @@ class TestParameters: datavars.os.linux['shortname'].set('GL') assert PARAMS['test'].value == 'Gentoo Linux -> GL' - # Переменные, указанные в to_set не меняются если используется + # Переменные, указанные в set_to не меняются если используется # дефолтное значение, рассчитанное с помощью переменных. assert datavars.os.linux.subname == 'KDE' @@ -222,7 +222,7 @@ class TestParameters: short='Test parameter', full='Test parameters are needed for tests.'), shortname='t' - ).bind('os.linux.fullname').to_set( + ).bind('os.linux.fullname').set_to( 'os.linux.shortname', 'os.hashvar.value2')) @@ -265,7 +265,7 @@ class TestParameters: short='Test bool parameter', full=('Test bool parameter, that can' ' disactivate other parameter')), - shortname='b').to_set('os.linux.test_2'), + shortname='b').set_to('os.linux.test_2'), StringTestParameter('test-string', 'Strings', Description( short='Test string parameter', @@ -586,7 +586,7 @@ class TestParameters: Description(short='Test bool', full=('Test parameters are' ' needed for tests.') - )).to_set('os.linux.test_2')) + )).set_to('os.linux.test_2')) assert PARAMS['test-1'].disactivity_comment is None PARAMS.set_parameters({'test-1': 'choice_2', 'test-2': True}) @@ -1001,7 +1001,7 @@ class TestParameters: Description(short=('Test table'), full=('Test parameters are' ' needed for tests.')), - shortname='t').to_set('os.dev_table')) + shortname='t').set_to('os.dev_table')) assert PARAMS['test'].value.get_for_var() ==\ [{'dev': '/dev/sdb1', 'mount': '/'}, {'dev': '/dev/sdb2', 'mount': '/var/calculate'}, @@ -1075,7 +1075,7 @@ class TestParameters: Description(short=('Test table'), full=('Test parameters are' ' needed for tests.')), - shortname='t').to_set('os.dev_table')) + shortname='t').set_to('os.dev_table')) assert PARAMS['test'].value.get_for_var() ==\ [{'dev': '/dev/sdb1', 'mount': '/', 'name': 'Device 1', 'status': 'mounted'}, diff --git a/tests/scripts/test_scripts.py b/tests/scripts/test_scripts.py index efcf397..cf3e602 100644 --- a/tests/scripts/test_scripts.py +++ b/tests/scripts/test_scripts.py @@ -1,14 +1,38 @@ import os import shutil import pytest -from calculate.scripts.scripts import Var, Task, Static, TaskError, Done,\ - DoneAny, Success, SuccessAny, Failed,\ - FailedAny, Block, For, While, Until,\ - Handler, Script, Run, ScriptError,\ - ActionError, ScriptLauncher, RunTemplate -from calculate.variables.datavars import Namespace, Variable, IntegerType,\ - StringType, BooleanType, ListType,\ - HashType +from calculate.scripts.scripts import ( + Var, + Task, + Static, + TaskError, + Done, + DoneAny, + Success, + SuccessAny, + Failed, + FailedAny, + Block, + For, + While, + Until, + Handler, + Script, + Run, + ScriptError, + ActionError, + ScriptLauncher, + RunTemplates + ) +from calculate.variables.datavars import ( + Namespace, + Variable, + IntegerType, + StringType, + BooleanType, + ListType, + HashType + ) from calculate.variables.loader import Datavars from calculate.utils.io_module import IOModule @@ -164,7 +188,7 @@ class TestTasks(): 'success': True, 'error_message': None} - def test_if_script_object_is_created_with_one_correct_task_with_a_unfulfilled_condition__the_task_will_be_successfully_completed(self): + def test_if_script_object_is_created_with_one_correct_task_with_a_unfulfilled_condition__the_task_will_not_be_completed(self): Namespace.reset() datavars = Namespace.datavars @@ -1874,11 +1898,11 @@ class TestTasks(): 'variables')) Script('test_script', - ).tasks(RunTemplate(id="templates_1", - action='action_1', - package="test-category/test-package", - chroot_path=TESTFILES_PATH, - root_path="/etc"), + ).tasks(RunTemplates(id="templates_1", + action='action_1', + package="test-category/test-package", + chroot_path=TESTFILES_PATH, + root_path="/etc"), ).make_launcher(IOModule(), datavars, None)() assert 'test_script' in datavars.scripts @@ -1899,11 +1923,11 @@ class TestTasks(): assert 'test_3' in datavars.os.linux Script('test_script', - ).tasks(RunTemplate(id="templates_1", - action='action_2', - package="test-category/test-package", - chroot_path=TESTFILES_PATH, - root_path="/etc"), + ).tasks(RunTemplates(id="templates_1", + action='action_2', + package="test-category/test-package", + chroot_path=TESTFILES_PATH, + root_path="/etc"), ).make_launcher(IOModule(), datavars, datavars.os)() assert 'test_script' in datavars.scripts diff --git a/tests/server/test_responses.py b/tests/server/test_responses.py new file mode 100644 index 0000000..43a60c5 --- /dev/null +++ b/tests/server/test_responses.py @@ -0,0 +1,232 @@ +import pytest +from calculate.server.utils.responses import ResponseStructure + + +@pytest.mark.responses +@pytest.mark.parametrize('case', [ + { + "name": "One positional argument", + "args": ["Very important data"], + "kwargs": {}, + "result": {"data": "Very important data"} + }, + { + "name": "multiple positional args", + "args": [1, 2, 3], + "kwargs": {}, + "result": {"data": [1, 2, 3]} + }, + { + "name": "only keyword args", + "args": [], + "kwargs": {"name": "Calculate Server", + "version": "0.1"}, + "result": {"data": {"name": "Calculate Server", + "version": "0.1"}} + }, + { + "name": "Both positional and keyword args", + "args": [1, 2, 3], + "kwargs": {"name": "Calculate Server", + "version": "0.1"}, + "result": {"data": [1, 2, 3]} + }, + ], + ids=lambda case: case["name"]) +def test_creation_response_dictionary_using_only_add_data_method(case): + response = ResponseStructure("http://0.0.0.0:2007/") + response.add_data(*case["args"], **case["kwargs"]) + assert response.get_dict() == case["result"] + + +@pytest.mark.responses +@pytest.mark.parametrize('case', [ + { + "name": "Two calls with positional values", + "data": [ + {"args": ["Very important data 1"], + "kwargs": {}}, + {"args": ["Very important data 2"], + "kwargs": {}}, + ], + "result": {"data": ["Very important data 1", + "Very important data 2"] + }, + }, + { + "name": "Call with positional argument and with keywords one", + "data": [ + {"args": ["Very important data"], + "kwargs": {}}, + {"args": [], + "kwargs": {"name": "Calculate Server", + "version": "0.1"}}, + ], + "result": {"data": {"name": "Calculate Server", + "version": "0.1"} + }, + }, + { + "name": "Call with keyword arguments and with positional one", + "data": [ + {"args": [], + "kwargs": {"name": "Calculate Server", + "version": "0.1"}}, + {"args": ["Very important data"], + "kwargs": {}}, + ], + "result": {"data": "Very important data"}, + }, + { + "name": "Two calls with keyword arguments", + "data": [ + {"args": [], + "kwargs": {"name": "Calculate Server"}}, + {"args": [], + "kwargs": {"version": "0.1"}}, + ], + "result": {"data": {"name": "Calculate Server", + "version": "0.1"} + }, + }, + ], + ids=lambda case: case["name"]) +def test_multiple_add_data_method_callings(case): + response = ResponseStructure("http://0.0.0.0:2007/") + for data in case["data"]: + response.add_data(*data["args"], **data["kwargs"]) + assert response.get_dict() == case["result"] + + +@pytest.mark.responses +@pytest.mark.parametrize('case', [ + { + "name": "Passing not templated uri", + "action": "commands", + "uri": "/commands/", + "templated": False, + "result": { + "_links": { + "commands": { + "href": "http://0.0.0.0:2007/commands" + } + } + } + }, + { + "name": "Passing templated uri", + "action": "find_command", + "uri": "commands/{cl_command}", + "templated": True, + "result": { + "_links": { + "find_command": { + "href": "http://0.0.0.0:2007/commands/{cl_command}", + "templated": True + } + } + } + }, + ], + ids=lambda case: case["name"]) +def test_creation_response_dictionary_using_only_add_links_method(case): + response = ResponseStructure("http://0.0.0.0:2007/") + response.add_link(case["action"], case["uri"], case["templated"]) + assert response.get_dict() == case["result"] + + +@pytest.mark.responses +@pytest.mark.parametrize('case', [ + { + "name": "One embed method call", + "embed": { + "menu": + (ResponseStructure("http://0.0.0.0:2007/"). + add_data(name="Calculate Server", version="0.1"). + add_link("get_commands", "commands"). + add_link("find_command", "commands/{cl_command}", + templated=True) + ).get_dict(), + }, + "result": { + "_embedded": { + "menu": { + "data": { + "name": "Calculate Server", + "version": "0.1" + }, + "_links": { + "get_commands": { + "href": "http://0.0.0.0:2007/commands" + }, + "find_command": { + "href": "http://0.0.0.0:2007/commands/{cl_command}", + "templated": True + }, + } + } + } + } + }, + { + "name": "Two embed method calls", + "embed": { + "first": + (ResponseStructure("http://0.0.0.0:2007/"). + add_data(id=1, name="first", description="First value"). + add_link("self", "values/1"). + add_link("find_value", "values/search/{value_name}", + templated=True) + ).get_dict(), + "second": + (ResponseStructure("http://0.0.0.0:2007/"). + add_data(id=2, name="second", description="Second value"). + add_link("self", "values/2"). + add_link("find_value", "values/search/{value_name}", + templated=True) + ).get_dict(), + }, + "result": { + "_embedded": { + "first": { + "data": { + "id": 1, + "name": "first", + "description": "First value", + }, + "_links": { + "self": { + "href": "http://0.0.0.0:2007/values/1" + }, + "find_value": { + "href": "http://0.0.0.0:2007/values/search/{value_name}", + "templated": True + }, + } + }, + "second": { + "data": { + "id": 2, + "name": "second", + "description": "Second value", + }, + "_links": { + "self": { + "href": "http://0.0.0.0:2007/values/2" + }, + "find_value": { + "href": "http://0.0.0.0:2007/values/search/{value_name}", + "templated": True + }, + } + } + } + } + }, + ], + ids=lambda case: case["name"]) +def test_creation_response_dictionary_using_only_embed_method(case): + response = ResponseStructure("http://0.0.0.0:2007/") + for embed_name, embed_data in case["embed"].items(): + response.embed(embed_name, embed_data) + assert response.get_dict() == case["result"] diff --git a/tests/server/test_server.py b/tests/server/test_server.py index cb5c2e6..2b631e1 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -13,8 +13,10 @@ test_client = TestClient(app) def authenticate(username: str, password: str): request_headers = {"accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"} - request_data = {"username": username, "password": password, - "grant_type": "password", "scope": None, "client_id": None, + request_data = {"username": username, + "password": password, + "grant_type": "password", + "scope": None, "client_id": None} token = test_client.post("/auth", data=request_data, json=request_headers) token_header = token.json() @@ -33,29 +35,29 @@ def test_to_make_testfiles(): symlinks=True) -@pytest.mark.server -def test_get_root_message(): - authorization_headers = authenticate("denis", "secret") - response = test_client.get("/", headers=authorization_headers) - assert response.status_code == 200 - assert response.json() == {"msg": "root msg"} +# @pytest.mark.server +# def test_get_root_message(): +# authorization_headers = authenticate("denis", "secret") +# response = test_client.get("/", headers=authorization_headers) +# assert response.status_code == 200 +# assert response.json() == {"msg": "root msg"} -@pytest.mark.server -def test_get_commands_list(): - authorization_headers = authenticate("denis", "secret") - response = test_client.get("/commands", headers=authorization_headers) - assert response.status_code == 200 - assert response.json() == {"test_1": - {"title": "Test 1", - "category": "Test Category", - "icon": "/path/to/icon_1.png", - "command": "test_1"}, - "test_2": - {"title": "Test 2", - "category": "Test Category", - "icon": "/path/to/icon_2.png", - "command": "cl_test_2"}} +# @pytest.mark.server +# def test_get_commands_list(): +# authorization_headers = authenticate("denis", "secret") +# response = test_client.get("/commands", headers=authorization_headers) +# assert response.status_code == 200 +# assert response.json() == {"test_1": +# {"title": "Test 1", +# "category": "Test Category", +# "icon": "/path/to/icon_1.png", +# "command": "test_1"}, +# "test_2": +# {"title": "Test 2", +# "category": "Test Category", +# "icon": "/path/to/icon_2.png", +# "command": "cl_test_2"}} # @pytest.mark.server diff --git a/tests/templates/test_template_executor.py b/tests/templates/test_template_executor.py index c98e9ff..fd5383a 100644 --- a/tests/templates/test_template_executor.py +++ b/tests/templates/test_template_executor.py @@ -1666,7 +1666,7 @@ def test_if_append_join_file_method_s_input_is_a_template_with_protected_target_ @pytest.mark.template_executor -def test_if_append_join_file_method_s_input_is_a_template_with_protected_target_path_to_an_unexisting_file_that_should_exist_and_autoupdate_parameters_is_set__the_method_creates_new_empty_file_joins_a_template_with_them_and_adds_it_to_config_file(): +def test_if_append_join_file_method_s_input_is_a_template_with_protected_target_path_to_an_unexisting_file_that_should_exist_and_autoupdate_parameter_is_set__the_method_creates_new_empty_file_joins_a_template_with_them_and_adds_it_to_config_file(): target_path = join_paths( CHROOT_PATH, '/etc/append_join_file_testfiles/file_2') diff --git a/tests/templates/test_template_filters.py b/tests/templates/test_template_filters.py index 996b2de..ab403e5 100644 --- a/tests/templates/test_template_filters.py +++ b/tests/templates/test_template_filters.py @@ -1,10 +1,12 @@ import pytest import calculate.templates.template_filters as filters + class TestObj: def __str__(self): return "TestObj_value" + @pytest.mark.parametrize('case', [ { diff --git a/tests/templates/testfiles/test_executor_root/var/db/pkg/test-category/test-package-1.0/CONTENTS b/tests/templates/testfiles/test_executor_root/var/db/pkg/test-category/test-package-1.0/CONTENTS index c9d62a7..369ca03 100644 --- a/tests/templates/testfiles/test_executor_root/var/db/pkg/test-category/test-package-1.0/CONTENTS +++ b/tests/templates/testfiles/test_executor_root/var/db/pkg/test-category/test-package-1.0/CONTENTS @@ -56,6 +56,7 @@ dir /etc/append_link_dir_testfiles/link_dir_6 obj /etc/append_link_dir_testfiles/link_dir_6/file d41d8cd98f00b204e9800998ecf8427e 1592491327 dir /etc/append_join_file_testfiles obj /etc/append_join_file_testfiles/file_1 ee090b452dbf92d697124eb424f5de5b 1592552158 +obj /etc/append_join_file_testfiles/file_2 ee090b452dbf92d697124eb424f5de5b 1592552158 obj /etc/append_join_file_testfiles/file_4 ee090b452dbf92d697124eb424f5de5b 1592552158 obj /etc/append_join_file_testfiles/file_5 ee090b452dbf92d697124eb424f5de5b 1592574626 obj /etc/append_join_file_testfiles/file_6 ee090b452dbf92d697124eb424f5de5b 1592574626 diff --git a/tests/variables/test_calculateini.py b/tests/variables/test_calculateini.py index 7b1986e..45ee96c 100644 --- a/tests/variables/test_calculateini.py +++ b/tests/variables/test_calculateini.py @@ -3,6 +3,7 @@ from calculate.variables.loader import CalculateIniParser, Define @pytest.mark.vars +@pytest.mark.calculateini class TestCalculateIni: def test_section_values(self): ini_parser = CalculateIniParser()