Русский
Русский
English
Статистика
Реклама

Config

VaultPydantic продолжение саги, локальная разработка

16.12.2020 02:21:17 | Автор: admin


Предыстория


Предыдущая статья


В предыдущей статье я писал о том, как сконфигурировать ваше приложение с помощью Sitri, однако, упустил момент с локальной разработкой, так как согласитесь, что не очень удобно разворачивать vault локально, да и хранить локальный конфиг в общем vault, особенно, если над проектом работают несколько человек неудобнее вдвойне.


В Sitri данная проблема решается достаточно просто с помощью локального режима для ваших классов настроек, то есть вам не придётся даже переписывать ничего или дублировать код, да и структура json файла для локального режима будет почти полностью повторять структуру секретов.


Итак, ну а теперь, давайте добавим в наш проектик буквально пару строк кода + я покажу, как со всем этим можно работать, если ваш проект локально запускается в docker-compose.


Готовим код


Для начала, давайте договоримся, что local_mode действует тогда, когда ENV = "local" :) Далее, я предлагаю немного отредактировать наш provider_config.py и создать там класс BaseConfig от которого мы будем наследовать в наших настройках Config классы. Мы сделаем это для того, чтобы не дублировать код, то есть в самих классах настроек будет только то, что специфично для них.


import hvacfrom sitri.providers.contrib.system import SystemConfigProviderfrom sitri.providers.contrib.vault import VaultKVConfigProviderfrom sitri.settings.contrib.vault import VaultKVSettingsconfigurator = SystemConfigProvider(prefix="superapp")ENV = configurator.get("env")is_local_mode = ENV == "local"local_mode_file_path = configurator.get("local_mode_file_path")def vault_client_factory() -> hvac.Client:    client = hvac.Client(url=configurator.get("vault_api"))    client.auth_approle(        role_id=configurator.get("role_id"),        secret_id=configurator.get("secret_id"),    )    return clientprovider = VaultKVConfigProvider(    vault_connector=vault_client_factory,    mount_point=f"{configurator.get('app_name')}/{ENV}",)class BaseConfig(VaultKVSettings.VaultKVSettingsConfig):    provider = provider    local_mode = is_local_mode    local_provider_args = {"json_path": local_mode_file_path}

Немного про local_provider_args в этом поле мы указываем аргументы для создания экземпляра JsonConfigProvider, они будут провалидированы и данный словарь должен соотвествовать схеме, поэтому не волнуйтесь это не какой-то грязный приём. Однако, если вы хотите создать экземпляр локального провайдера сами, то вы просто положить его в опциональное поле local_provider.


Теперь, мы можем спокойно наследовать конфиг классы от базового. Например, класс настроек для соединения с Kafka будет выглядеть так:


from typing import Any, Dictfrom pydantic import Fieldfrom sitri.settings.contrib.vault import VaultKVSettingsfrom superapp.config.provider_config import BaseConfig, configuratorclass KafkaSettings(VaultKVSettings):    mechanism: str = Field(..., vault_secret_key="auth_mechanism")    brokers: str = Field(...)    auth_data: Dict[str, Any] = Field(...)    class Config(BaseConfig):        default_secret_path = "kafka"        default_mount_point = f"{configurator.get('app_name')}/common"        local_mode_path_prefix = "kafka"

Как видите требуемые изменения минимальны. local_mode_path_prefix мы указываем, чтобы в нашем json файле сохранялась структура общего конфига. А теперь, давайте напишем этот самый json файл для локальной конфигурации:


{    "db":    {        "host": "testhost",        "password": "testpassword",        "port": 1234,        "user": "testuser"    },    "faust":    {        "agents":        {            "X":            {                "concurrency": 2,                "partitions": 5            }        },        "app_name": "superapp-workers",        "default_concurrency": 5,        "default_partitions_count": 10    },    "kafka":    {        "auth_data":        {            "password": "testpassword",            "username": "testuser"        },        "brokers": "kafka://test",        "mechanism": "SASL_PLAINTEXT"    }}

Ну, или просто скопипастим его из конца прошлой статьи. Как видите, тут всё весьма просто. Для наших дальнейших изысканий, переименуем main.py в корне проекта на __main__.py, чтобы можно было запустить пакет командой из docker-compose.


Запихиваем приложение в контейнер и наслаждаемся сборкой


Первое, что мы должны сделать это написать небольшой Dockerfile:


FROM python:3.8.3-busterENV PYTHONUNBUFFERED=1 \    POETRY_VIRTUALENVS_CREATE=false \    POETRY_VIRTUALENVS_IN_PROJECT=false \    POETRY_NO_INTERACTION=1RUN pip install poetryRUN mkdir /superapp/WORKDIR /superapp/COPY ./pyproject.toml ./poetry.lock /superapp/RUN poetry install --no-ansiWORKDIR /

Здесь мы просто устанавливаем зависимости и всё, так как он для локальной разработки, то код проекта мы не копируем.


Далее, нам понадобиться env-файл с необходимыми для local-mode переменными:


SUPERAPP_ENV=localSUPERAPP_LOCAL_MODE_FILE_PATH=/config.jsonSUPERAPP_APP_NAME=superapp

Как видите, ничего лишнего, никакой конфигурационной информации для Vault не нужно, так как при локальном режиме, приложение даже не попытается постучаться в Vault.


Ну и последнее, что мы должны написать сам docker-compose.yml файл:


# docker-compose config for local developmentversion: '3'services:  superapp:    command: python3 -m superapp    restart: always    build:      context: ./      dockerfile: Dockerfile    volumes:      - ./superapp:/superapp      - ./config.json:/config.json    env_file:      - .env.local

Как видите, тут тоже всё просто. Наш json-файл мы кладём в корень, как и указали выше в переменной окружения для контейнера.


Ну а теперь, запуск:


docker-compose up

Creating article_sitri_vault_pydantic_superapp_1 ... doneAttaching to article_sitri_vault_pydantic_superapp_1superapp_1  | db=DBSettings(user='testuser', password='testpassword', host='testhost', port=1234) faust=FaustSettings(app_name='superapp-workers', default_partitions_count=10, default_concurrency=5, agents={'X': AgentConfig(partitions=5, concurrency=2)}) kafka=KafkaSettings(mechanism='SASL_PLAINTEXT', brokers='kafka://test', auth_data={'password': 'testpassword', 'username': 'testuser'})superapp_1  | {'db': {'user': 'testuser', 'password': 'testpassword', 'host': 'testhost', 'port': 1234}, 'faust': {'app_name': 'superapp-workers', 'default_partitions_count': 10, 'default_concurrency': 5, 'agents': {'X': {'partitions': 5, 'concurrency': 2}}}, 'kafka': {'mechanism': 'SASL_PLAINTEXT', 'brokers': 'kafka://test', 'auth_data': {'password': 'testpassword', 'username': 'testuser'}}}

Как видите, всё успешно запустилось и информация из нашего json-фала успешно прошла все проверки и стала настройками для локальной версии приложения, юхху!


Код этой статьи я положил в отдельную ветку, так что можете зайти, склонить и протестировать всё самолично :)
branch

Подробнее..

Конфигурируем сервис с помощью Vault и Pydantic

09.12.2020 04:19:27 | Автор: admin

image


Предисловие


В данной статье я расскажу о конфигурации для вашей сервисов с помощью связки Vault (KV и пока только первой версии, т.е. без версионирования секретов) и Pydantic (Settings) под патронажем Sitri.


Итак, допустим, что у нас есть приложение superapp с заведёнными конфигами в Vault и аутентификацией с помощью approle, примерно так настроим (настройку policies для доступа к секрет-энжайнам и к самим секретам я оставлю за кадром, так как это достаточно просто и статья не об этом):


Key                        Value---                        -----bind_secret_id             truelocal_secret_ids           falsepolicies                   [superapp_service]secret_id_bound_cidrs      <nil>secret_id_num_uses         0secret_id_ttl              0stoken_bound_cidrs          []token_explicit_max_ttl     0stoken_max_ttl              30mtoken_no_default_policy    falsetoken_num_uses             50token_period               0stoken_policies             [superapp_service]token_ttl                  20mtoken_type                 default

Прим.: естественно, что если у вас есть возможность и приложение выходит в боевой режим, то secret_id_ttl лучше делать не бесконечным, выставляя 0 секунд.


SuperApp требует конфигурации: подключения к базе данных, подключение к kafka и faust конфигурации для работы кластера воркеров.


Подготовим Sitri


В базовой документации библиотеки есть простой пример, конфигурирования через vault-провайдер, однако, он не охватывает все возможности и может быть полезным, если ваше приложение конфигурируется достаточно легко.


Итак, для начала сконфигурируем vault-провайдер в условном файле provider_config.py:


import hvac  from sitri.providers.contrib.vault import VaultKVConfigProvider  from sitri.providers.contrib.system import SystemConfigProvider  configurator = SystemConfigProvider(prefix="superapp")  ENV = configurator.get("env")  def vault_client_factory() -> hvac.Client:      client = hvac.Client(url=configurator.get("vault_api"))      client.auth_approle(          role_id=configurator.get("role_id"),    secret_id=configurator.get("secret_id"),    )      return client  provider = VaultKVConfigProvider(      vault_connector=vault_client_factory, mount_point=f"{configurator.get('app_name')}/{ENV}"  )

В данном случае мы достаём из среды с помощью системного провайдера несколько переменных для конфигурирования подключения к vault, т.е. изначально должны быть экспортированы следующие переменные:


export SUPERAPP_ENV=devexport SUPERAPP_APP_NAME=superappexport SUPERAPP_VAULT_API=https://your-vault-host.domainexport SUPERAPP_ROLE_ID=535b268d-b858-5fb9-1e3e-79068ca77e27 # Примерexport SUPERAPP_SECRET_ID=243ab423-12a2-63dc-3d5d-0b95b1745ccf # Пример

В примере предполагается, что базовый mount_point к вашим секретам для определённой среды будет содержать имя приложения и имя среды, поэтому мы и экспортировали SUPERAPP_ENV. Путь до секретов отдельных частей приложения мы будем определять в settings-классах далее, поэтому в провайдере secret_path мы оставляем пустым.


Классы настроек


Начнём по пунктам и разнесём три класса настроек (БД, Kafka, Faust) по трём разным файлам.


Настройки БД


from pydantic import Field  from sitri.settings.contrib.vault import VaultKVSettings  from superapp.config.provider_config import provider  class DBSettings(VaultKVSettings):      user: str = Field(..., vault_secret_key="username")      password: str = Field(...)      host: str = Field(...)      port: int = Field(...)      class Config:          provider = provider          default_secret_path = "db"

Итак, как видите, конфиг. данные для базы у нас достаточно простые. Этот класс будет по-умолчанию смотреть в секрет superapp/dev/db, так, как мы указали в config классе, в остальном здесь простые pydantic поля, но в одном из них присутствует extra-аргумент vault_secret_key он нужен тогда, когда ключ в секрете не совпадает по имени с pydantic полем в нашем классе, если его не указывать, то провайдер будет искать ключ по имени поля.


Например, в нашем тестовом приложении, предполагается, что в секрете superapp/dev/db, есть ключи password и username, но мы хотим, чтобы последний был помещён в поле user для удобства и краткости.


Поместим в вышеозначенный секрет следующие данные для примера:


{  "host": "testhost",  "password": "testpassword",  "port": "1234",  "username": "testuser"}

Для первого класса из тройки, я покажу, как легко можно всё это запустить, чтобы данные собрались сами:


db_settings = DBSettings()pprint(db_settings.dict())# -> # {#     "host": "testhost",#     "password": "testpassword",#     "port": 1234,#     "user": "testuser"# }

Настройки Kafka


from typing import Dict, Any  from pydantic import Field  from sitri.settings.contrib.vault import VaultKVSettings  from superapp.config.provider_config import provider, configurator  class KafkaSettings(VaultKVSettings):      mechanism: str = Field(..., vault_secret_key="auth_mechanism")      brokers: str = Field(...)      auth_data: Dict[str, Any] = Field(...)      class Config:          provider = provider          default_secret_path = "kafka"          default_mount_point = f"{configurator.get('app_name')}/common"

В данном случае, представим, что инстанс кафки для разных сред нашего сервиса один, поэтому секрет хранится по пути superapp/common/kafka


{  "auth_data": "{\"password\": \"testpassword\", \"username\": \"testuser\"}",  "auth_mechanism": "SASL_PLAINTEXT",  "brokers": "kafka://test"}

Класс настройки поймёт комплексный тип данных Dict[str, Any] и распарсит его в словарь, то есть при заполнении наших настроек будут следующие данные:


{    "auth_data":    {        "password": "testpassword",        "username": "testuser"    },    "brokers": "kafka://test",    "mechanism": "SASL_PLAINTEXT"}

Так же, если секрет будет задан напрямую в json, например так:


{  "auth_data": {    "password": "testpassword",    "username": "testuser"  },  "auth_mechanism": "SASL_PLAINTEXT",  "brokers": "kafka://test"}

То класс настроек тоже сможет правильно разложить данные.


P.S.
Так же, secret_path и mount_point можно задавать на уровне полей, чтобы провайдер запросил конкретные значения из разных секретов (если это требуется). Приведу цитату с приоритезацией пути секрета и точки монтирования из документации:


Secret path prioritization:
  1. vault_secret_path (Field arg)
  2. default_secret_path (Config class field)
  3. secret_path (provider initialization optional arg)


Mount point prioritization:
  1. vault_mount_point (Field arg)
  2. default_mount_point (Config class field)
  3. mount_point (provider initialization optional arg)


Настройки Faust и отдельных воркеров


from typing import Dict  from pydantic import Field, BaseModel  from sitri.settings.contrib.vault import VaultKVSettings  from superapp.config.provider_config import provider  class AgentConfig(BaseModel):      partitions: int = Field(...)      concurrency: int = Field(...)  class FaustSettings(VaultKVSettings):      app_name: str = Field(...)      default_partitions_count: int = Field(..., vault_secret_key="partitions_count")      default_concurrency: int = Field(..., vault_secret_key="agent_concurrency")      agents: Dict[str, AgentConfig] = Field(default=None, vault_secret_key="agents_specification")      class Config:          provider = provider          default_secret_path = "faust"

superapp/dev/faust:


{  "agent_concurrency": "5",  "app_name": "superapp-workers",  "partitions_count": "10"}

В данном случае, по-умолчанию у нас есть глобальные значения кол-ва партиций в кафке и concurrency для агентов. Таким образом, по-умолчанию наши настройки будут выгружены так:


{  "agents": None,  "app_name": "superapp-workers",  "default_concurrency": 5,  "default_partitions_count": 10}

Например, у нас есть агент X с настройками:


{  "partitions": 5,  "concurrency": 2}

Наш секрет в связи с этим должен выглядеть следующим образом:


{  "agent_concurrency": "5",  "agents_specification": {    "X": {      "concurrency": "2",      "partitions": "5"    }  },  "app_name": "superapp-workers",  "partitions_count": "10"}

Как и ожидалось данные корректно смапились и типы значений были преобразованы так, как указано в модели AgentConfig:


{    "agents":    {        "X":        {            "concurrency": 2,            "partitions": 5        }    },    "app_name": "superapp-workers",    "default_concurrency": 5,    "default_partitions_count": 10}

Совмещаем в единый конфиг класс


from pydantic import BaseModel, Field  from superapp.config.database_settings import DBSettings  from superapp.config.faust_settings import FaustSettings  from superapp.config.kafka_settings import KafkaSettings  class AppSettings(BaseModel):      db: DBSettings = Field(default_factory=DBSettings)      faust: FaustSettings = Field(default_factory=FaustSettings)      kafka: KafkaSettings = Field(default_factory=KafkaSettings)

Совместим наши классы настроек в одну модель, применив default_factory для автоматического сбора при инициализации модели всех наших данных.


Давайте запустим наш код и проверим, как всё сработается вместе:


from superapp.config import AppSettings  config = AppSettings()  print(config)  print(config.dict())

Получаем общий вывод всей конфигурации приложения:


db=DBSettings(user='testuser', password='testpassword', host='testhost', port=1234) faust=FaustSettings(app_name='superapp-workers', default_partitions_count=10, default_concurrency=5, agents={'X': AgentConfig(partitions=5, concurrency=2)}) kafka=KafkaSettings(mechanism='SASL_PLAINTEXT', brokers='kafka://test', auth_data={'password': 'testpassword', 'username': 'testuser'})

{    "db":    {        "host": "testhost",        "password": "testpassword",        "port": 1234,        "user": "testuser"    },    "faust":    {        "agents":        {            "X":            {                "concurrency": 2,                "partitions": 5            }        },        "app_name": "superapp-workers",        "default_concurrency": 5,        "default_partitions_count": 10    },    "kafka":    {        "auth_data":        {            "password": "testpassword",            "username": "testuser"        },        "brokers": "kafka://test",        "mechanism": "SASL_PLAINTEXT"    }}

Счастье, радость, восторг!


У нас получилась такая структура тест-проекта:


superapp config    app_settings.py    database_settings.py    faust_settings.py    __init__.py    kafka_settings.py    provider_config.py __init__.py main.py

Послесловие


Как видите настройка достаточно проста с Sitri, после неё мы получаем чёткую схему конфигурации с нужными нам типами данных у значений, даже если в vault по-умолчанию они хранились строками.


Пишите комментарии по поводу либы, кода или общие впечатления. Буду рад любому отзыву!


P.S. Код из статьи я залил на github https://github.com/Egnod/article_sitri_vault_pydantic

Подробнее..

Из песочницы IOptions и его друзья

20.06.2020 00:17:13 | Автор: admin

Во время разработки часто возникает потребность для вынесения параметров в конфигурационные файлы. Да и вообще хранить разные конфигурационный константы в коде является признаком дурного тона. Один из вариантов хранения настроек использования конфигурационных файлов. .Net Core из коробки умеет работать с такими форматами как: json, ini, xml и другие. Так же есть возможность писать свои провайдеры конфигураций. (Кстати говоря за работу с конфигурациями отвечает сервис IConfiguration и IConfigurationProvider для доступа к конфигурациям определенного формата и для написания своих провайдеров)


image


Но главная сложность, на мой взгляд, состоит в том, что у нас имеются аж 3 разные интерфейса для работы с конфигурациями. И не всегда понятно для чего они нужны и какой когда нужно использовать.


На MSDN есть статья, которая должна раскрывать все вопросы. Но, как всегда, не все так просто.


IOptions


Does not support:
Reading of configuration data after the app has started.
Named options

Is registered as a Singleton and can be injected into any service lifetime.

Вцелом, из описания все сразу становится ясно: загружает данные из конфигурационного файла при старте приложения, не подтягивает никаких изменений.


Но у меня большие претензии к Microsoft в плане нейминга. На мой взгляд, если человек недостаточно знаком с технологией, или вообще видит ее в первый раз, то данный интерфейс это то, что первое придет на ум для использования. И потом, вероятно далеко не сразу, человек выяснит, что то все работает совсем не так, как он задумывал.


Я привык, что довольно большая часть конфигураций в моих проектах может довольно часто меняться. И лично мне хотелось бы, что бы интерфейс, который буквально кричит о том, что его надо использовать для конфигурации вел себя более очевидно. (Хотя конечно я могу быть не прав, и это только мои придирки. А большая часть людей использует файлы конфигураций по-другому)


IOptionsSnapshot


Is useful in scenarios where options should be recomputed on every request

Is registered as Scoped and therefore cannot be injected into a Singleton service.

Supports named options

Должен подходить для большинства ситуаций, о которых я писал выше. Обновляет информацию о конфигурации при каждом запросе. И что немаловажно, не изменяет ее во время запроса.
Подходит для множества случаев, например feature toggle.


MSDN нам говорит, что не может быть заинжекчен в Singletone на самом деле может (это прям тема для отдельного поста), но тогда и сам он начинает себя вести как Singletone.


IOptionsMonitor


Is used to retrieve options and manage options notifications for TOptions instances.

Is registered as a Singleton and can be injected into any service lifetime.

Supports:
Change notifications
Named options
Reloadable configuration
Selective options invalidation (IOptionsMonitorCache)

По сути это доступ к вашим конфигурациям в режиме реального времени. Тут стоит быть осторожным. И если вы в процессе какого-то запроса читаете конфигурацию несколько раз стоит быть готовым, что она может измениться.


IOptionsMonitorCache интерфейс для построения обычного кэша на базе IOptionsMonitor.


Практика


Все тесты проводились на следующем окружении


sw_versProductName:    Mac OS XProductVersion: 10.15.5BuildVersion:   19F101dotnet --version3.1.301

Мы посмотрели, что говорит нам документация. Теперь давайте посмотри как это работает.


Сразу оставлю ссылку на код, если кто-то захочет ознакомиться подробнее.


В качестве примера будет простое Web API


 public class Program {     public static void Main(string[] args)     {         CreateHostBuilder(args).Build().Run();     }     public static IHostBuilder CreateHostBuilder(string[] args) =>         Host.CreateDefaultBuilder(args)             .ConfigureWebHostDefaults(webBuilder =>             {                 webBuilder.UseKestrel();                 webBuilder.UseStartup<Startup>();                 webBuilder.UseUrls("http://*:5010/");             })             .UseDefaultServiceProvider(options => options.ValidateScopes = false); }

Клиент, который будет к нему обращаться


 static async Task Main(string[] args) {     using var client = new HttpClient();     var prevResponse = String.Empty;     while (true)     {         var response = await client.GetStringAsync("http://localhost:5010/settings");         if (response != prevResponse) // пишем в консоль только, если настройки изменились         {             Console.WriteLine(response);             prevResponse = response;         }     } }

В Web API создаем 3 сервиса, который принимает все 3 варианта конфигураций в конструктор и возвращают текущее значение.


private readonly IOptions<TestGroupSettings> _testOptions;private readonly IOptionsSnapshot<TestGroupSettings> _testOptionsSnapshot;private readonly IOptionsMonitor<TestGroupSettings> _testOptionsMonitor;public ScopedService(IOptions<TestGroupSettings> testOptions, IOptionsSnapshot<TestGroupSettings> testOptionsSnapshot,    IOptionsMonitor<TestGroupSettings> testOptionsMonitor){    _testOptions = testOptions;    _testOptionsSnapshot = testOptionsSnapshot;    _testOptionsMonitor = testOptionsMonitor;}

Сервисы будут 3х скоупов: Singletone, Scoped и Transient.


public void ConfigureServices(IServiceCollection services){    services.Configure<TestGroupSettings>(Configuration.GetSection("TestGroup"));    services.AddSingleton<ISingletonService, SingletonService>();    services.AddScoped<IScopedService, ScopedService>();    services.AddTransient<ITransientService, TransientService>();    services.AddControllers();}

В процессе работы нашего Web Api изменяем значение TestGroup.Test файла appsettings.json


Имеем следующую картину:
Сразу после запуска


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0SingletonService IOptionsMonitor value: 0ScopedService IOptions value: 0ScopedService IOptionsSnapshot value: 0ScopedService IOptionsMonitor value: 0TransientService IOptions value: 0TransientService IOptionsSnapshot value: 0TransientService IOptionsMonitor value: 0

Изменяем нашу настройку и получаем интересную картину
Сразу после изменения


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0 // не измениласьSingletonService IOptionsMonitor value: 0 // не измениласьScopedService IOptions value: 0ScopedService IOptionsSnapshot value: // стала пустойScopedService IOptionsMonitor value: 0 // не измениласьTransientService IOptions value: 0TransientService IOptionsSnapshot value: // стала пустойTransientService IOptionsMonitor value: 0 // не изменилась

Следующий вывод в консоль (конфиг больше не менялся)


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0 // не измениласьSingletonService IOptionsMonitor value: 0 // не измениласьScopedService IOptions value: 0ScopedService IOptionsSnapshot value: changed setting // измениласьScopedService IOptionsMonitor value: 0 // не измениласьTransientService IOptions value: 0TransientService IOptionsSnapshot value: changed setting // измениласьTransientService IOptionsMonitor value: 0 // не изменилась

Последний вывод (конфиг также не менялся)


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0 // не измениласьSingletonService IOptionsMonitor value: changed setting // измениласьScopedService IOptions value: 0ScopedService IOptionsSnapshot value: changed setting // измениласьScopedService IOptionsMonitor value: changed setting // измениласьTransientService IOptions value: 0TransientService IOptionsSnapshot value: changed setting // измениласьTransientService IOptionsMonitor value: changed setting // изменилась

Что имеем в итоге? А имеем то, что IOptionsMonitor не такой шустрый, как нам говорит документация. Как можно заметить IOptionsSnapshot может вернуть пустое значение. Но, он работает быстрее, чем IOptionsMonitor.


Пока не особо понятно откуда берется это пустое значение. И самое интересное, что подобное поведение проявляется не всегда. Как-то через раз в моем примере IOptionsMonitor и IOptionsSnapshot отрабатывают одновременно.


Выводы


Если вам нужно передавать конфигурацию, которые никогда в процессе жизни вашего приложения не будут меняться используете IOptions.


Если ваши конфигурации будут меняться, то тут все как всегда зависит. Если Вам важно, что бы в скоупе вашего запроса настройки были релевантны на момент запроса, IOptionsSnapshot ваш выбор (но не для Singletone, в нем значение никогда не изменится). Но стоит учитывать его странности, хотя и столкнуться с ними вряд ли вам придется.


Если же вам нужны наиболее актуальные значения (или почти) используйте IOptionsMonitor.


Буду рад, если вы запустите пример у себя, и расскажете, повторяется подобное поведение или нет. Возможно мы имеем баг на MacOS, а может это by design.


Продолжу разбираться с этой темой, а пока завел issue, может там прояснят такое поведение.

Подробнее..

Второй HDMI монитор к Raspberry Pi3 через DPI интерфейс и FPGA плату

21.10.2020 10:10:51 | Автор: admin

На этом видео показаны: плата Raspberry Pi3, к ней, через разъем GPIO, подключена FPGA плата Марсоход2rpi (Cyclone IV), к которой подключен HDMI монитор. Второй монитор подключен через штатный разъем HDMI Raspberry Pi3. Все вместе работает, как система с двумя мониторами.
Дальше расскажу, как это реализовано.

На популярной плате Raspberry Pi3 есть разъем GPIO, через который можно подключать разные платы расширения: датчики, светодиоды, драйвера шаговых двигателей и многое другое. Конкретная функция каждого вывода на разъеме зависит от конфигурации портов. Конфигурация GPIO ALT2 позволяет переключить разъем в режим DPI интерфейса, Display Parallel Interface. Существуют платы расширения для подключения VGA мониторов, через DPI. Однако, во-первых, мониторы VGA уже не так распространены, как HDMI, а во-вторых, цифровой интерфейс все лучше аналогового. Тем более, что ЦАП на подобных VGA платах расширения обычно выполнен в виде R-2-R цепочек и часто не более 6 бит на цвет.

В режиме ALT2 пины разъема GPIO имеют следующее значение:
image

Я здесь раскрасил RGB выводы разъема соответственно в красный, зеленый и синий цвета. Другие важные сигналы это сигналы синхронизации развертки V-SYNC и H-SYNC, а так же CLK. Тактовая частота CLK это частота, с которой значения пикcелей выдаются на разъем, она зависит от выбранного видеорежима.

Для подключения цифрового HDMI монитора нужно захватить сигналы DPI интерфейса и преобразовать их в сигналы HDMI. Сделать это можно, например, с помощью какой либо FPGA платы. Как оказалось, плата Марсоход2rpi подходит для этих целей. По правде говоря, основной вариант подключения этой платы через специальный переходник выглядит вот так:
image
Эта плата служит для увеличения числа GPIO портов и для подключение большего числа периферийных устройств к raspberry. При этом, 4 сигнала GPIO при таком подключении используются под JTAG сигналы, так, что программа из распберри может загружать FPGA прошивку в ПЛИС. Из-за этого такое штатное подключение мне не подходит, выпадают 4 DPI сигнала. По счастью, дополнительные гребеночки на плате имеют совместимую с Raspberry распиновку. Так, что я могу развернуть плату на 90 градусов и все равно подключить ее к моей малинке:

Конечно, придется использовать внешний JTAG программатор, но это не проблема.

Небольшая проблема все же есть. Не каждый вывод FPGA может использоваться, как вход тактовой частоты. Есть только несколько dedicated pin, которые можно использовать для этих целей. Так и здесь получилось, что GPIO_0 сигнал CLK не попадает на ввод FPGA, который возможно использовать как вход тактовой частоты ПЛИС. Так что все таки пришлось кинуть один проводок на платку. Я соединяю GPIO_0 и сигнал KEY[1] платы:
image
Теперь расскажу немного про проект в ПЛИС. Основная сложность при формировании HDMI сигналов это очень высокие частоты. Если посмотреть на цоколевку разъема HDMI, то видно, что сигналы RGB теперь являются последовательными дифференциальными сигналами:


Использование дифференциального сигнала позволяет бороться с синфазными помехами на линии передачи. При этом, исходный восьмибитный код каждого сигнала цвета преобразуется в 10-ти битный TMDS (Transition-minimized differential signaling). Это специальный способ кодирования для удаления постоянной составляющей из сигнала и минимизации переключений сигналов в дифференциальной линии. Поскольку на один байт цвета теперь по последовательной линии передачи нужно передать 10 бит, то получается, что тактовая частота сериализатора должна быть в 10 раз выше, чем тактовая частота пикселей. Если взять к примеру видео режим 1280х720 60Гц, то частота пикселей у такого режима 74,25МГц. На сериализаторе должно быть 742,5МГц. Обычные FPGA вообще-то на такое, к сожалению, не способны. Однако, по нашему счастью, в FPGA имеются встроенные выводы DDIO. Это такие выводы, которые уже как бы являются сериализаторами 2-к-1. То есть они могут выдавать последовательно два бита по фронту и спаду тактовой частоты. Значит в проекте FPGA можно использовать не 740МГц, а 370МГц, но нужно задейcтвовать выходные элементы DDIO в ПЛИС. Вот 370МГц уже вполне достижимая частота. К сожалению, режим 1280x720 это предел. Более высокого разрешения в нашей FPGA Cyclone IV установленной на плате Марсоход2rpi не достичь.

Итак, в проекте, входная частота пикселей CLK поступает на PLL, где умножается на 5. На этой частоте байты R, G, B, преобразуются в пары бит. Это делает TMDS энкодер. Исходных код на Verilog HDL выглядит вот так:
module hdmi(input wire pixclk,// 74MHzinput wire clk_TMDS2,// 370MHzinput wire hsync,input wire vsync,input wire active,input wire [7:0]red,input wire [7:0]green,input wire [7:0]blue,output wire TMDS_bh,output wire TMDS_bl,output wire TMDS_gh,output wire TMDS_gl,output wire TMDS_rh,output wire TMDS_rl);wire [9:0] TMDS_red, TMDS_green, TMDS_blue;TMDS_encoder encode_R(.clk(pixclk), .VD(red  ), .CD({vsync,hsync}), .VDE(active), .TMDS(TMDS_red));TMDS_encoder encode_G(.clk(pixclk), .VD(green), .CD({vsync,hsync}), .VDE(active), .TMDS(TMDS_green));TMDS_encoder encode_B(.clk(pixclk), .VD(blue ), .CD({vsync,hsync}), .VDE(active), .TMDS(TMDS_blue));reg [2:0] TMDS_mod5=0;  // modulus 5 counterreg [4:0] TMDS_shift_bh=0, TMDS_shift_bl=0;reg [4:0] TMDS_shift_gh=0, TMDS_shift_gl=0;reg [4:0] TMDS_shift_rh=0, TMDS_shift_rl=0;wire [4:0] TMDS_blue_l  = {TMDS_blue[9],TMDS_blue[7],TMDS_blue[5],TMDS_blue[3],TMDS_blue[1]};wire [4:0] TMDS_blue_h  = {TMDS_blue[8],TMDS_blue[6],TMDS_blue[4],TMDS_blue[2],TMDS_blue[0]};wire [4:0] TMDS_green_l = {TMDS_green[9],TMDS_green[7],TMDS_green[5],TMDS_green[3],TMDS_green[1]};wire [4:0] TMDS_green_h = {TMDS_green[8],TMDS_green[6],TMDS_green[4],TMDS_green[2],TMDS_green[0]};wire [4:0] TMDS_red_l   = {TMDS_red[9],TMDS_red[7],TMDS_red[5],TMDS_red[3],TMDS_red[1]};wire [4:0] TMDS_red_h   = {TMDS_red[8],TMDS_red[6],TMDS_red[4],TMDS_red[2],TMDS_red[0]};always @(posedge clk_TMDS2)beginTMDS_shift_bh <= TMDS_mod5[2] ? TMDS_blue_h  : TMDS_shift_bh  [4:1];TMDS_shift_bl <= TMDS_mod5[2] ? TMDS_blue_l  : TMDS_shift_bl  [4:1];TMDS_shift_gh <= TMDS_mod5[2] ? TMDS_green_h : TMDS_shift_gh  [4:1];TMDS_shift_gl <= TMDS_mod5[2] ? TMDS_green_l : TMDS_shift_gl  [4:1];TMDS_shift_rh <= TMDS_mod5[2] ? TMDS_red_h   : TMDS_shift_rh  [4:1];TMDS_shift_rl <= TMDS_mod5[2] ? TMDS_red_l   : TMDS_shift_rl  [4:1];TMDS_mod5 <= (TMDS_mod5[2]) ? 3'd0 : TMDS_mod5+3'd1;endassign TMDS_bh = TMDS_shift_bh[0];assign TMDS_bl = TMDS_shift_bl[0];assign TMDS_gh = TMDS_shift_gh[0];assign TMDS_gl = TMDS_shift_gl[0];assign TMDS_rh = TMDS_shift_rh[0];assign TMDS_rl = TMDS_shift_rl[0];endmodulemodule TMDS_encoder(input clk,input [7:0] VD,// video data (red, green or blue)input [1:0] CD,// control datainput VDE,  // video data enable, to choose between CD (when VDE=0) and VD (when VDE=1)output reg [9:0] TMDS = 0);wire [3:0] Nb1s = VD[0] + VD[1] + VD[2] + VD[3] + VD[4] + VD[5] + VD[6] + VD[7];wire XNOR = (Nb1s>4'd4) || (Nb1s==4'd4 && VD[0]==1'b0);wire [8:0] q_m = {~XNOR, q_m[6:0] ^ VD[7:1] ^ {7{XNOR}}, VD[0]};reg [3:0] balance_acc = 0;wire [3:0] balance = q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7] - 4'd4;wire balance_sign_eq = (balance[3] == balance_acc[3]);wire invert_q_m = (balance==0 || balance_acc==0) ? ~q_m[8] : balance_sign_eq;wire [3:0] balance_acc_inc = balance - ({q_m[8] ^ ~balance_sign_eq} & ~(balance==0 || balance_acc==0));wire [3:0] balance_acc_new = invert_q_m ? balance_acc-balance_acc_inc : balance_acc+balance_acc_inc;wire [9:0] TMDS_data = {invert_q_m, q_m[8], q_m[7:0] ^ {8{invert_q_m}}};wire [9:0] TMDS_code = CD[1] ? (CD[0] ? 10'b1010101011 : 10'b0101010100) : (CD[0] ? 10'b0010101011 : 10'b1101010100);always @(posedge clk) TMDS <= VDE ? TMDS_data : TMDS_code;always @(posedge clk) balance_acc <= VDE ? balance_acc_new : 4'h0;endmodule

Потом выходные пары подаются на DDIO выход, который последовательно выдает однобитный сигнал по фронту и спаду.

Сам DDIO можно было бы описать таким Verilog кодом:
module ddio(input wire d0,input wire d1,input wire clk,output wire out);reg r_d0;reg r_d1;always @(posedge clk)beginr_d0 <= d0;r_d1 <= d1;endassign out = clk ? r_d0 : r_d1;endmodule

Но так работать скорее всего не будет. Нужно использовать альтеровскую мегафункцию ALTDDIO_OUT, чтобы на самом деле задействовать выходные DDIO элементы. В моем проекте используется именно библиотечный компонент ALTDDIO_OUT.

Возможно, все это выглядит немного мудрено, но работает.
Посмотреть весь исходный код, написанный на Verilog HDL, можно вот здесь, на github.

Скомпилированная прошивка для FPGA зашивается в EPCS чип, установленный на плате Марсоход2rpi. Таким образом, при подаче питания на плату FPGA, ПЛИС будет инициализироваться из флэш памяти и стартовать.

Теперь нужно немного рассказать о конфигурации самого Raspberry.
Я делаю эксперименты на Raspberry PI OS (32 bit) based on Debian Buster, Version:August 2020,
Release date:2020-08-20, Kernel version:5.4.

Нужно сделать две вещи:
  • отредактировать файл config.txt;
  • создать конфигурацию X сервера для работы с двумя мониторами.

При редактировании файла /boot/config.txt нужно:
  1. выключить использование i2c, i2s, spi;
  2. включить режим DPI с помощью оверлея dtoverlay=dpi24;
  3. настроить видеорежим 1280x720 60Гц, 24 бита на точку на DPI;
  4. указать необходимое количество фреймбуфферов 2 (max_framebuffers=2, только тогда появится второе устройство /dev/fb1)


Полный текст файла config.txt выглядит так.
# For more options and information see
# http://rpf.io/configtxt
# Some settings may impact device functionality. See link above for details

# uncomment if you get no picture on HDMI for a default "safe" mode
#hdmi_safe=1

# uncomment this if your display has a black border of unused pixels visible
# and your display can output without overscan
disable_overscan=1

# uncomment the following to adjust overscan. Use positive numbers if console
# goes off screen, and negative if there is too much border
#overscan_left=16
#overscan_right=16
#overscan_top=16
#overscan_bottom=16

# uncomment to force a console size. By default it will be display's size minus
# overscan.
#framebuffer_width=1280
#framebuffer_height=720

# uncomment if hdmi display is not detected and composite is being output
hdmi_force_hotplug=1

# uncomment to force a specific HDMI mode (this will force VGA)
#hdmi_group=1
#hdmi_mode=1

# uncomment to force a HDMI mode rather than DVI. This can make audio work in
# DMT (computer monitor) modes
#hdmi_drive=2

# uncomment to increase signal to HDMI, if you have interference, blanking, or
# no display
#config_hdmi_boost=4

# uncomment for composite PAL
#sdtv_mode=2

#uncomment to overclock the arm. 700 MHz is the default.
#arm_freq=800

# Uncomment some or all of these to enable the optional hardware interfaces
#dtparam=i2c_arm=on
#dtparam=i2s=on
#dtparam=spi=on

dtparam=i2c_arm=off
dtparam=spi=off
dtparam=i2s=off

dtoverlay=dpi24
overscan_left=0
overscan_right=0
overscan_top=0
overscan_bottom=0
framebuffer_width=1280
framebuffer_height=720
display_default_lcd=0
enable_dpi_lcd=1
dpi_group=2
dpi_mode=87
#dpi_group=1
#dpi_mode=4
dpi_output_format=0x6f027
dpi_timings=1280 1 110 40 220 720 1 5 5 20 0 0 0 60 0 74000000 3

# Uncomment this to enable infrared communication.
#dtoverlay=gpio-ir,gpio_pin=17
#dtoverlay=gpio-ir-tx,gpio_pin=18

# Additional overlays and parameters are documented /boot/overlays/README

# Enable audio (loads snd_bcm2835)
dtparam=audio=on

[pi4]
# Enable DRM VC4 V3D driver on top of the dispmanx display stack
#dtoverlay=vc4-fkms-v3d
max_framebuffers=2

[all]
#dtoverlay=vc4-fkms-v3d
max_framebuffers=2



После этого, нужно создать конфигурационный файл для X сервера для использования двух мониторов на двух фреймбуфферах /dev/fb0 и /dev/fb1
Мой файл конфигурации /usr/share/x11/xorg.conf.d/60-dualscreen.conf такой
Section "Device"
Identifier "LCD"
Driver "fbturbo"
Option "fbdev" "/dev/fb0"
Option "ShadowFB" "off"
Option "SwapbuffersWait" "true"
EndSection

Section "Device"
Identifier "HDMI"
Driver "fbturbo"
Option "fbdev" "/dev/fb1"
Option "ShadowFB" "off"
Option "SwapbuffersWait" "true"
EndSection

Section "Monitor"
Identifier "LCD-monitor"
Option "Primary" "true"
EndSection

Section "Monitor"
Identifier "HDMI-monitor"
Option "RightOf" "LCD-monitor"
EndSection

Section "Screen"
Identifier "screen0"
Device "LCD"
Monitor "LCD-monitor"
EndSection

Section "Screen"
Identifier "screen1"
Device "HDMI"
Monitor "HDMI-monitor"
EndSection

Section "ServerLayout"
Identifier "default"
Option "Xinerama" "on"
Option "Clone" "off"
Screen 0 "screen0"
Screen 1 "screen1" RightOf "screen0"
EndSection


Ну и, если еще не установлена, то нужно установить Xinerama. Тогда пространство рабочего стола будет полноценно расширено на два монитора, как показано выше на демо ролике.
Вот пожалуй и все. Теперь, и владельцы Raspberry Pi3 смогут пользоваться двумя мониторами.

Описание и схему платы Марсоход2rpi можно посмотреть вот здесь.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru