Package: veil-api-client
VeiL api clientПредназначен для упрощения интеграции между конечным приложением/скриптом и ECP VeiL. Если вы не хотите слишком глубоко
погружаться в детали и нюансы работы с сессиями, сущностями и конечным API VeiL - рассмотрите работу через данную
библиотеку. Внутри мы используемaiohttp client.УстановкаПроект доступен в PyPi, можете воспользоваться поддерживаемым пакетным менеджером, например,pippip install veil-api-clientИспользованиеПредусмотрено два возможных сценария использования: постоянная работа и разовый запуск с завершением. Ниже будет пример
простого и продвинутого использования.ДокументацияКраткое описание вы читаете сейчас, более подробное доступна через help, например, help(response).
Логика импортов построена так, что конечные сущности с которыми предлагается работать доступны напрямую из корня.
Краткое описание классов ниже:# Сущности клиента
from veil_api_client import VeilClient, VeilClientSingleton 

# Дополнительные параметры клиента для продвинутого использования
from veil_api_client import VeilCacheConfiguration, VeilRetryConfiguration 

# Сущность пагинатора в ответе
from veil_api_client import VeilRestPaginator

# Интерфейсы для аргументов сущностей на VeiL ECP
from veil_api_client import DomainConfiguration, VeilGuestAgentCmd, DomainTcpUsbРазовый запускЕсли ваш сценарий использования это асинхронный скрипт, который должен выполниться и завершить свою работу,
то лучше всего использовать менеджер контекста.ПримерыНапример, Вам нужно получить список из 10 ВМ на ECP VeiL и для каждой выполнить команду start:import asyncio
from veil_api_client import VeilClient, VeilRestPaginator
logging.basicConfig(level=logging.DEBUG)  # debug message can be useful

token_115 = 'jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2V'
server_address_115 = '192.168.11.115'

async def simple_main():
    async with VeilClient(server_address=server, token=token) as session:
        # Настраиваем пагинатор - сортировка по полю verbose_name, первые 10 записей.
        paginator = VeilRestPaginator(ordering='verbose_name', limit=10)
        # У каждой сессии есть атрибут - сущность на ECP VeiL.
        veil_domain_entity = session.domain()
        # получаем ответ со списком вм
        veil_response = await veil_domain_entity.list(paginator=paginator)

        # Ответ списком имеет 200й статус, ответ на результат задачи скорее всего будет в 202 статусе
        if veil_response.status_code == 418:
            raise AssertionError('VeiL ECP проходит процедуру обновления. Попробуйте позже.')
        elif not veil_response.success:
            raise AssertionError('Ошибка получения информации от VeiL.')

        # Включаем каждую из полученных ВМ
        for domain in veil_response.response:
            start_response = await domain.start()
            if not start_response.success:
                raise AssertionError(start_response.error_detail)

loop = asyncio.get_event_loop()
loop.run_until_complete(simple_main())Примеры использования тэгов# Список тэгов
tags_response = await session.tag().list()

# Создание тэга
new_tag1 = TagConfiguration(verbose_name='tag9')
tag1_response = await session.tag().create(new_tag1)
if tag1_response.success:
    task = tag1_response.task  # Таска создания нового тега
    new_uid = task.first_entity  # идентификатор создаваемой сущности

# Информация по тэгу
tag_response = await session.tag(new_uid).info()
# Сущность тега
tag = tag_response.response[0]

# Прикрепление сущности к тэгу
entity = VeilEntityConfiguration(entity_uuid='01747df8-bdc6-4a50-9747-3e59ef5d4868',
                                 entity_class='domain')
entity_response = await tag.add_entity(entity_configuration=entity)
if entity_response.success:
    print('Успешное прикрепление')

# Прикрепление нескольких сущностей к тэгу
entity1 = VeilEntityConfiguration(entity_uuid='01747df8-bdc6-4a50-9747-3e59ef5d4868',
                                  entity_class='domain')
entity2 = VeilEntityConfiguration(entity_uuid='67e45225-db3f-4474-b334-e8d83ef89a2a',
                                  entity_class='domain')
entity_response = await tag.add_entities(entities_conf=[entity1, entity2])
if entity_response.success:
     print('Успешное множественное прикрепление')

# Редактирование тэга
update_response = await tag.update(colour='#ff0000', verbose_name='newname')
if update_response.success:
     print('Успешное редактирование')

# Открепление сущности от тэга
tag = session.tag('581d2c2b-1f1b-4807-953a-c0e80e8a943b')
await tag.info()#
entity = VeilEntityConfiguration(entity_uuid='01747df8-bdc6-4a50-9747-3e59ef5d4868',
                                 entity_class='domain')

entity_response = await tag.remove_entity(entity_configuration=entity)
if entity_response.success:
    print('Успешное открепление')

# Открепление сущностей от тэга
tag = session.tag('3cacf9f1-7107-45de-bd9d-55d8d6e7eeb3')
await tag.info()
entity1 = VeilEntityConfiguration(entity_uuid='01747df8-bdc6-4a50-9747-3e59ef5d4868',
                                  entity_class='domain')
entity2 = VeilEntityConfiguration(entity_uuid='67e45225-db3f-4474-b334-e8d83ef89a2a',
                                  entity_class='domain')
entity_response = await tag.remove_entities(entities_conf=[entity1, entity2])
if entity_response.success:
     print('Успешное множественное открепление')

# Удаление
remove_response = await tag.remove()
if remove_response.success:
     print('Успешное удаление')Пример для изменения золотого образа (шаблона)domain = session.domain('67e45225-db3f-4474-b334-e8d83ef89a2a')
await domain.change_template()Пример получения полного перечня возможных полей DomainПо умолчанию VeiL ECP имеет разные наборы данных доступные через методыinfoиlist. Чтобы
получить расширенный набор атрибутов в методеlistнеобходимо передать__all__как единственный элемент
массиваfields.response = await domain.list(fields=['__all__', ])
for domain in response.response:
    print('{}:{}:{}'.format(domain.verbose_name, domain.cpu_type, domain.spice_stream))Получение конкретного перечня возможных полей DomainВ ситуации, когда необходимо сократить перечень возвращаемых полей методомlistпередайте список
нужных атрибутов в аргументfieldsметодаinfo.response = await domain.list(fields=['verbose_name', 'status', 'memory_count', 'id'])
for domain in response.response:
    print('{}:{}:{}:{}'.format(domain.verbose_name, domain.status, domain.memory_count, domain.api_object_id))ПримечаниеОбратите внимание, что__all__и перечень полей это взаимоисключающие параметры.
Также необходимо иметь в виду, что некоторые поля можно получить только с__all__.В примерах выше работа с ответами выполнена через атрибут.response. В двух словах это массив из 1 или нескольких
объектов вызывающей сущности, например, если запрос выполняется для domain.list внутри будут находиться объекты VeilDomainExt
с теми аргументами, которые были на момент ответа.Если необходимо работать напрямую с ответом VeiL ECP - используете атрибутdata(описан ниже).Постоянно запущенное приложениеЕсли ваше приложение запущено постоянно и ему необходима +- постоянная связь с VeiL ECP, стоит доработатьVeilClientSingleton. Основное отличие этого сценария - будет храниться ранее созданный инстанс клиента для
конкретного контроллера. Это сократит возможные дублирования сессий, временные затраты на установку соединения
для запросов с маленьким временным интервалом и унифицирует настройки.
При таком сценарии использования — слежение за закрытием сессий перекладывается на ваше приложение.
Если этот сценарий Вам кажется ближе, задумайтесь о расширении готовой логики VeilClientSingleton.Ниже пример, который может пригодиться:import asyncio
import json
import uvloop
import logging
from typing import Optional

from pymemcache.client.base import Client as MemcachedClient
from veil_api_client import (VeilClient, VeilClientSingleton,
                             VeilRetryConfiguration, VeilCacheConfiguration, VeilDomainExt,
                             VeilCacheAbstractClient)

logging.basicConfig(level=logging.DEBUG)


# Длинная часть с переопределением и дополнением базовой логики
class DictSerde:
    """Сериализатор для записи данных ответов в кэш."""

    @staticmethod
    def serialize(key, value):
        """Serialize VeilApiResponse to bytes."""
        if isinstance(value, str):
            return value.encode('utf-8'), 1
        elif isinstance(value, dict):
            return json.dumps(value).encode('utf-8'), 2
        raise Exception('Unknown serialization format')

    @staticmethod
    def deserialize(key, value, flags):
        """Deserialize bytes to dict."""
        if flags == 1:
            return value.decode('utf-8')
        elif flags == 2:
            return json.loads(value.decode('utf-8'))
        raise Exception('Unknown serialization format')


class UserCacheClient(VeilCacheAbstractClient):
    """Реализация пользовательского кэш-клиента."""

    def __init__(self):
        self.client = MemcachedClient(server=('localhost', 11211), serde=DictSerde())

    async def get_from_cache(self,
                             veil_api_client_request_cor,
                             veil_api_client,
                             method_name,
                             url: str,
                             headers: dict,
                             params: dict,
                             ssl: bool,
                             json_data: Optional[dict] = None,
                             retry_opts: Optional[VeilRetryConfiguration] = None,
                             ttl: int = 0,
                             *args, **kwargs):
        """Метод, который вызовет клиент.

        Внутри себя должен вызывать запись в кэш и чтение из кэша.
        """
        # cache key can`t contain spaces
        cache_key = url.replace(' ', '')
        # Получаем данные из кэша
        cached_result = self.client.get(cache_key)
        # Если данные есть - возвращаем
        if cached_result:
            return cached_result
        # Если в кэше нет актуальных данных - получаем результат запроса
        result_dict = await veil_api_client_request_cor(veil_api_client,
                                                        method_name, url, headers,
                                                        params, ssl, json_data,
                                                        retry_opts, *args, **kwargs)
        # Т.к. ответ преобразуется в VeilApiResponse после вызова этого метода в result_dict будет лежать ответ aiohttp
        if isinstance(result_dict, dict) and result_dict.get('status_code', 0) in (200, 201, 202, 204):
            try:
                # пытаемся сохранить результат в кэш
                await self.save_to_cache(cache_key, result_dict, ttl)
            except Exception as ex_msg:
                print('Failed to save response to cache: {}'.format(ex_msg))
        # Возвращаем ответ aiohttp
        return result_dict

    async def save_to_cache(self, key, data, ttl: int):
        """Сохраняем результат в кэш."""
        return self.client.set(key, data, expire=ttl)


class UserRequestError(AssertionError):
    pass


class UserDomain:

    @staticmethod
    async def get(id_):
        return {'key': 'value {}'.format(id_)}


class UserVeilClient(VeilClient):
    """Переопределяем встроенный клиент."""

    async def api_request(self, *args, **kwargs):

        response = await super().api_request(*args, **kwargs)
        if not hasattr(response, 'status_code'):
            raise ValueError('Response is broken. Check veil_api_client version.')

        url = kwargs.get('url')
        params = kwargs.get('params')
        api_object = kwargs.get('api_object')

        if hasattr(api_object, 'api_object_id') and api_object.api_object_id:
            if response.status_code == 404 and isinstance(api_object, VeilDomainExt):
                vm_object = await UserDomain.get(api_object.api_object_id)
                print('vm object:', vm_object.items())

        if not response.success:
            error_description = 'url: {}\nparams: {}\nresponse:{}'.format(url, params, response.data)
            raise UserRequestError(error_description)

        return response


class UserVeilClientSingleton(VeilClientSingleton):
    """Переопределяем менеджер соединений."""

    __client_instances = dict()

    def __init__(self) -> None:
        """Please see help(VeilClientSingleton) for more info."""
        self.__TIMEOUT = 10
        self.__CACHE_OPTS = VeilCacheConfiguration(ttl=30, cache_client=UserCacheClient())
        self.__RETRY_OPTS = VeilRetryConfiguration()

    def add_client(self, server_address: str, token: str, retry_opts=None) -> 'VeilClient':
        """Пре конфигурированное подключение."""

        if server_address not in self.__client_instances:
            instance = UserVeilClient(server_address=server_address,
                                      token=token,
                                      session_reopen=True,
                                      timeout=self.__TIMEOUT,
                                      ujson_=True,
                                      cache_opts=self.__CACHE_OPTS,
                                      retry_opts=self.__RETRY_OPTS)
            self.__client_instances[server_address] = instance
        return self.__client_instances[server_address]

    @property
    def instances(self) -> dict:
        """Show all instances of VeilClient."""
        return self.__client_instances


# Конец переопределения и начало клиента
async def extra_main():
    token = 'jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo1LCJ1c2VybmFtZSI6ImRldnlhdGtpbl92ZGkiLCJleHAiOjE5MjY2Nzk4OTUsInNzbyI6ZmFsc2UsIm9yaWdfaWF0IjoxNjEyMTgzODk1fQ.UKf7Sr4JB6tQw8Ftn5Cex0ZO6Qg4uNG24827fohWVXM'
    server = '192.168.11.102'

    # Настраиваем клиент для будущего использования
    # При проверке статуса проверяется именно тот статус, который пришел в ответе. Предположим мы хотим повторы для всех
    # вм, что не удалось найти
    veil_client = UserVeilClientSingleton()

    # Добавляем подключение к первому контроллеру
    session1 = veil_client.add_client(server, token)

    # Получаем информацию о конкретной ВМ
    veil_domain = session1.domain()
    await veil_domain.list()

    # Берем ответы из кеша
    await veil_domain.list()
    await veil_domain.list()
    await veil_domain.list()
    await veil_domain.list()

    # Перед завершением приложения закрываем все используемые сессии
    instances = veil_client.instances
    for inst in instances:
        await instances[inst].close()


uvloop.install()
loop = asyncio.get_event_loop()
loop.run_until_complete(extra_main())КонфигурацияЕсли вам потребовалось использовать сетевые запросы, то с большой долей вероятности Вы захотите использовать uvloop.
Пример установки можно посмотретьтутБольшинство значений передаваемых по умолчанию являются оптимальными настройками VeiL VDI, однако может потребоваться их
изменить, например, по умолчанию в библиотеке используется ujson, поэтому если Вы расширяете написанные методы,
имейте в виду ограничения, которые он накладывает, либо переопределите опцию ujson_ на False.Сущности, к которым предоставляется доступ и их методыЛюбой объект клиента имеет интерфейсы:Кластер - VeilClient.cluster()Контроллер - VeilClient.controller()Пул данных - VeilClient.data_pool()Домен (Виртуальная машина) - VeilClient.domain()Нода (узел, сервер) - VeilClient.node()Задача - VeilClient.task()Виртуальный диск - VeilClient.vdisk()Тэг - VeilClient.tag()Библиотека - VeilClient.library()Журнал - VeilClient.event()Если при инициализации сущности не был передан id сущности, то есть возможность работы только с обобщенными методами
вроде list(). Если вы хотите иметь доступ к методам конкретной сущности — необходимо указать ее id.Основные методы сущностейlist() - асинхронный метод для получения списка сущностей на VeiL, использует переопределяемый пагинатор. Если сущностей
больше 100 - вам необходимо самостоятельно настроить limit для пагинатора, иначе будет 100.info() - получение информации о конкретной сущности. После вызова этого метода Вы можете использовать как атрибут.valueтак и конкретные атрибуты у сущности, например domain.servicepublic_attrs - список всех публичных атрибутов. После получения info они будут обновлены.uuid_ - конвертирует str с id в uuid.creating - результат операция сравнения статуса сущностиactive - результат операция сравнения статуса сущностиfailed - результат операция сравнения статуса сущностиdeleting - результат операция сравнения статуса сущностиservice - результат операция сравнения статуса сущностиpartial - результат операция сравнения статуса сущностиПараметры для подключения SPICE/VNCСущностьVeilDomainExtобладает двумя интерфейсами для получения данных подключения к ВМ.
Результатом выполнения обоих методов будет инстансDomainRemoteConnectionConfigurationсо следующими атрибутами:connection_url - "сырой" url, который вернул контроллерconnection_type - тип соединения (VNC/SPICE)host -host-часть из аргументов запроса вconnection_urlpassword -password-часть из аргументов запроса вconnection_urltoken -token-часть из аргументов запроса вconnection_url(внутриpath)SPICEspice = await domain.spice_conn()VNCvnc = await domain.vnc_conn()Основные конфигурируемые параметры VeilClient:server_address - адрес контроллера VeiL (без указания протокола, мы сами подставим https)token - токен интеграции VeiLsession_reopen - автоматически открывать сессию aiohttp.ClientSession, если она закрыта.timeout - aiohttp.ClientSession общий таймаутextra_headers - дополнительные заголовки для сессии (расширяющие или переопределяющие стандартные заголовки)extra_params - дополнительные параметры запроса для сессии (расширяющие или переопределяющие стандартные параметры)ujson_ - использовать или нет ujson, или json, опции aiohttp.client. Если в запросах проблемы, попробуйте отключитьКонфигурируемые параметры VeilClientSingleton:Мы намеренно сократили конфигурируемые параметры для данного класса, в целях облегчения и оптимизации запросов. Если
вы хотите что-то расширить - сделайте собственный класс по аналогии либо запросите доработку через issue.server_address - адрес контроллера VeiL (без указания протокола, мы сами подставим https)token - токен интеграции VeiLtimeout - aiohttp.ClientSession общий таймаутcache_opts - VeilCacheConfigurationretry_opts - VeilRetryConfiguration%ConfigurationДля дополнительной валидации (и из-за отсутствия дата классов) конфигурируемые параметры вынесены в отдельные
классы-потомки супер-класса VeilAbstractConfiguration. Ниже идет их перечисление.VeilCacheConfigurationВозможность передать пользовательский кэш.cache_client: инстанс пользовательского кэш-клиента, который сохраняет и читает данные из кэша.ttl: срок хранения данных в кэше. Если указать 0 - кэш не будет использоваться.VeilRetryConfigurationОпции для повторов запросов. Если указаны, клиент будет автоматически выполнять повтор по условиям описанным ниже.num_of_attempts - количество повторовtimeout - время ожидания перед повтором (с экспоненциальным ростом)max_timeout - максимальное время ожидания между попыткамиtimeout_increase_step - шаг увеличения времени ожиданияstatus_codes - статусы ответа запросов для повторовexceptions - исключения ответа запросов для повторовVeilEntityConfigurationСтруктура VeiL ECP для доступа к сущностям.entity_uuid - уникальный идентификатор сущности.entity_class - тип сущности (пул данных, вм и т.п.)TagConfigurationУпрощенное описание сущности Tag на VeiL ECP.verbose_name - не уникальная строкаcolour - строка содержащая hex-представление цветаVeilRestPaginatorУпрощенный интерфейс доступа к пагинатору VeiL ECP.name - полеnameordering - порядок сортировкиlimit - ограничение на количество записей в выдачеoffset - смещение блоков выдачиDomainBackupConfigurationПараметры для создания бэкапа виртуальной машины на VeiL ECP.backup - идентификатор предыдущего бэкапа.datapool - идентификатор пула данных на котором будет выполнено хранение
(может быть указан только если не передан backup).can_be_incremental - атрибут указывающий на возможность инкремента в будущем.increment - инкриминировать последний созданных бэкап.exclude_iso - исключить из бэкапа подключенные ISO-образы к виртуальной машины.DomainConfigurationУпрощенное описание параметров копирования виртуальной машины (domain) на VeiL ECP.Используется как аргумент domain_configuration в методеVeilDomainExt.createverbose_name - имя создаваемой ВМ.resource_pool - пул ресурсов VeiL ECP для создания ВМ.node - узел VeiL ECP для создания ВМ.datapool - пул данных VeiL ECP для создания ВМ.parent - шаблон VeiL ECP на основе которого необходимо выполнить копирование ВМ.thin - новая ВМ будет являться тонким клоном ее родителя.count - количество ВМ для созданияDomainCloneConfigurationУпрощенное описание параметров клонирования виртуальной машины (domain) на VeiL ECP.Используется как аргумент domain_configuration в методеVeilDomainExt.cloneverbose_name - имя создаваемой ВМ.resource_pool - пул ресурсов VeiL ECP для создания ВМ.node - узел VeiL ECP для создания ВМ.datapool - пул данных VeiL ECP для создания ВМ.snapshot - шаблон VeiL ECP на основе которого необходимо выполнить копирование ВМ.count - количество ВМ для созданияstart_on - включить создаваемые ВМDomainUpdateConfigurationУпрощенное описание параметров редактирования виртуальной машины (domain) на VeiL ECP.Используется как аргумент domain_update_configuration в методеVeilDomainExt.updateverbose_name - новое имя.description - новое описание.os_type - новый тип ОС.os_version - новая версия ОС.tablet - признак tablet.start_on_boot - признак start_on_boot.spice_stream - признак spice_stream.Основные атрибуты сущностейapi_object_prefix - указывает к какой сущности мы будем обращаться на VeiL ECPapi_object_id - идентификатор сущности на VeiL ECP, обычно UUID4Основные атрибуты ответа (VeilApiResponse)data - стандартный атрибут aiohttp.clientstatus_code - стандартный атрибут aiohttp.clientheaders - стандартный атрибут aiohttp.clientsuccess - признак успешности запроса (статус коды 200, 201, 202, 204)task - если запрос был поставлен в очередь на VeiL ECP тут будет сущность задачи, которую можно ожидатьresponse - список 1-М сущностей к которым происходило обращениеerror_code - код ошибки на VeiL, если запрос успешен, код будет 0error_detail - текстовое сопровождение ошибки на VeiLpaginator_count - count из ответа пагинатора, актуально для запросов вида list()paginator_next - url следующей страницы с ответами пагинатора, актуально для запросов вида list()paginator_previous - url предыдущей страницы с ответами пагинатора, актуально для запросов вида list()Как принять участие в проектеФорк -> изменения в отдельной ветке -> запустите тесты и разместите PR/MR.