
Хабр, привет!
Сегодня рассказываем об еще одном нашем кейсе с NSX-T. В прошлый раз мы написали о проблеме долгой реализации портов. Технические вводные остались те же: NSX используется для построения микросегментированной сети в кластерах Kubernetes + Kyverno. Взаимодействие K8s и NSX реализовано при помощи плагина VMware NCP. Что случилось в этот раз? Команда этого заказчика обновила продукт до версии 4.1., после чего обратилась к нам с проблемой перехода IP pool в состояние Failed. Иллюстрируем:

Диагностика
На начальном этапе анализа мы увидели, что проблема возникала в момент редактирования настроек любого из пулов, созданных до момента обновления. Так как данная инсталляция NSX использовалась для создания автоматизированных микросегментированных сетей кластера Kubernetes с помощью NCP, то проблема носила глобальный характер (сам масштаб мы увидим позже). Более того, она препятствовала нормальному созданию и изменению настроек каждого из тысяч подов.
Пошли смотреть NCP. К сожалению, с его стороны при попытке создания пода увидели только ошибки типа Unhandled Exception, что мало говорило об ошибках в NSX:
NSX 8 - [nsx@6876 comp="nsx-container-ncp" subcomp="ncp" level="WARNING"] nsx_ujo.common.controller NamespaceController worker 2 failed to sync b273350c-0d80-4c04-af31-f824fcc032a3 due to unexpected exception Traceback (most recent call last): File "/usr/local/lib/python3.8/dist-packages/nsx_ujo/common/controller.py", line 469, in worker self.sync_handler(key) File "/usr/local/lib/python3.8/dist-packages/nsx_ujo/ncp/k8s/namespace_controller.py", line 116, in sync_handler self._sync_namespace_update(namespace) File "/usr/local/lib/python3.8/dist-packages/nsx_ujo/common/metrics_utils.py", line 118, in wrapper return func(*args, **kwargs) File "<decorator-gen-2>", line 2, in _sync_namespace_update
В то же время со стороны NSX мы наблюдали следующую картину: сервис пытался пересоздать пул, но процесс завершался ошибкой из-за уже аллоцированных адресов:
1. 1970-01-01T09:32:48.390Z WARN providerTaskExecutor-1-63 RealizationFetchUtility 5795 POLICY [nsx@6876 comp="nsx-manager" level="WARNING" subcomp="manager"] Provider is not ready, realized object for /infra/realized-state/enforcement-points/default/ip-blocks/##########/ip-pools/ipp_#########################_0/block-subnets/ibs_#######################_0 is missing or incomplete 2. .... 3. 1970-01-01T09:32:48.391Z INFO providerTaskExecutor-1-63 RealizationFetchUtility 5795 POLICY [nsx@6876 comp="nsx-manager" level="INFO" subcomp="manager"] Added dependent paths:[/infra/ip-pools/ipp__#########################_0] on:/infra/ip-blocks//##########/ 4. 1970-01-01T09:32:48.391Z INFO providerTaskExecutor-1-63 PolicyIpamBaseProvider 5795 POLICY [nsx@6876 comp="nsx-manager" level="INFO" subcomp="manager"] IPAM - Realized resource not found with path:/infra/realized-state/enforcement-points/default/ip-blocks/##########/ip-pools/ipp_#########################_0/block-subnets/ibs_#######################_0 and type RZ_IP_ADDRESS_POOL_BLOCK_SUBNET_NEW 5.
Далее мы пытались оживить проблемные пулы через api. Однако все оказалось безуспешным: наши попытки (использовали PATCH и PUT) завершаются одной и той же ошибкой — Provider is not ready.
В результате мы выяснили, что пул штатными средствами может быть вылечен только полным выключением всей нагрузки. После высвобождения всех адресов пул «зеленел», и при включении нагрузки происходила аллокация из новой подсети. Этот вариант, что ожидаемо, никого не устраивал, потому что требовал выключения тысяч контейнеров — “back to the drawing board”.
И вот мы подошли к масштабу. В начале мы думали, что проблема затрагивала 30–40 пулов. По факту она негативно отражалась на работе более 1200 пулов:
А если посмотреть на проблемной площадке, мы видим, что 1213 пулов (ipp_) имеют всего 19 block_subnet: root@host# grep -A2 'REALIZED_ENTITY_TYPE_RZ_IP_ADDRESS_POOL' GenericPolicyRealizedResource.export | grep ipp_ | wc -l 2426 root@host# grep -A2 'REALIZED_ENTITY_TYPE_RZ_IP_ADDRESS_POOL' GenericPolicyRealizedResource.export | grep ibs_ | wc -l 1213 root@host# grep -c 'RZ_IP_ADDRESS_POOL_BLOCK_SUBNET' GenericPolicyRealizedResource.export 19
У этих пулов в процессе обновления потерялся статус реализации (готовности) объекта и аллоцированный сабнет. Соответственно, внутренняя логика NSX пыталась их восстановить, пересоздавая новый сабнет, но получала ошибку из-за наличия уже аллоцированных адресов. Это и препятствовало любым попыткам взаимодействия с пулами штатными средствами.
Соответственно, устраивающим всех (включая сам NSX) решением было бы засунуть эти записи в одну из нужных баз данных. Тем самым мы могли вернуть штатную реализацию пулов обратно.
На пути к решению — наконец-то!
В итоге изучения тестовых NSX Manager и базы заказчика мы собрали немного информации и выяснили:
Данные о реализации объектов хранятся в базе Corfu в таблице GenericPolicyRealizedResource;
Данные о каждой реализации объекта типа 'RZ_IP_ADDRESS_POOL_BLOCK_SUBNET' завязаны на каждом из шести индексов (таблиц) уже из другой БД — Elastic;
Каждый объект состоит из двух половин policy и manager. Несмотря на то, что VMware с релиза NSX 3.х заявляет о выводе в EoL-функционала Manager UI/API, в каждом, в том числе свежесозданном продукте версии 4.x, все еще присутствует этот функционал;
Несмотря на то что скрипт для взаимодействия с Corfu (corfu_tool_runner.py) не имеет функционала для добавления данных в инстанс базы, мы нашли его при анализе библиотек jvm. Оказалось, что org.corfudb.browser.CorfuStoreBrowserEditorMain имеет описанную функцию addRecord, которая принимает на ввод ключи: keyToAdd, valueToAdd, metadataToAdd.
Реализация
Исходя из пунктов выше, подготовили нужные json-объекты. С ними мы можем вернуть нормальную реализацию проблемных пулов. Дальше нужно понять структуру данных для объекта и зависимости, которые нам потребуются. Пример готового объекта:
Скрытый текст
Key: { "stringId": "/infra/realized-state/enforcement-points/default/ip-blocks/$ip_block/ip-pools/$ipp/block-subnets/$ibs"} Value: { "abstractPolicyRealizedResource": { "abstractPolicyResource": { "managedResource": { "displayName": "$ibs" }, "markedForDelete": false, "deleteWithParent": false, "locked": false, "isOnboarded": false, "internalKey": { "left": "$l_key", "right": "$r_key" }, "gmKey": { "left": "$l_key", "right": "$r_key" }, "parentPath": "infra/realized-state/enforcement-points/default/ip-blocks/$ip_block/ip-pools/$ipp/" }, "realizationObjectId": "$intent_uuid", "realizationState": "REALIZATION_STATE_REALIZED", "runtimeStatus": "RUNTIME_STATUS_UNINITIALIZED", "intentVersion": "$intent_version", "realizedVersionOnEnforcement": "0", "realizationApi": "/api/v1/pools/ip-subnets/$intent_uuid" }, "entityType": "REALIZED_ENTITY_TYPE_RZ_IP_ADDRESS_POOL_BLOCK_SUBNET", "extendedAttributes": { "start_ip": { "dataType": "DATA_TYPE_TYPE_STRING", "value": [ "$start_ip" ] }, "gateway_ip": { "dataType": "DATA_TYPE_TYPE_STRING", "value": [ "$gw" ] }, "cidr": { "dataType": "DATA_TYPE_TYPE_STRING", "value": [ "$CIDR" ] }, "end_ip": { "dataType": "DATA_TYPE_TYPE_STRING", "value": [ "$end_ip" ] } }, "intentPath": [ "/infra/ip-pools/$ipp/ip-subnets/$ibs" ] } Metadata: { "revision": "11", "createTime": "1660212755641", "createUser": "system", "lastModifiedTime": "1708445082785", "lastModifiedUser": "system" }
Где переменные:
ipp – policy индекс IP pool ibs – policy индекс IP block subnet (дочерняя сущность пула, описывающая выданный сабнет) ip_block – policy индекс IP block (родительская сущность глобального блока адресации) ip_block_mgr – manager индекс IP block ipp_mgr_id – manager индекс IP pool CIDR – CIDR адрес выделенной подсети l_key – уникальные ключи зависимости таблиц, в данном случае должны быть уникальными r_key – и не должны пересекаться с другими записями в пределах БД Corfu meta – метадата, версия ревизии должна соответствовать с объектом типа REALIZED_ENTITY_TYPE_RZ_IP_ADDRESS_POOL_BLOCK_SUBNET start_ip – стартовый IP сабнета end_ip – конечный IP сабнета
Итого, с понимаем, что нам нужно и выгрузки баз с проблемной инсталляцией, мы сводим итоговый csv-файл с необходимыми нам переменными.
Трудности
Тем не менее, в процессе реализации все оказалось несколько сложнее. Так, мало того, что данные были размазаны по двум разным базам elastic/corfu и десятку разных таблиц, между ними не существовало четкой связанности — вся работа по «склейке» готового объекта возлагалась на сам приклад.
Один и тот же объект, например IP pool, имеет несколько уникальных идентификаторов в elastic, причем иногда без указания явного пересечения. Так, если для Policy есть явные родительские отношения между IP pool и IP block subnet (_id последнего содержит явное указание на родителя):
"_index": "nsx_policy_ipaddresspool", "_type": "_doc", "_id": "/infra/ip-pools/ipp_f9fa99f4-6269-4124-a8e6-df245a9600c4_1", "_score": 1.0, "_source": { "check_overlap_with_existing_pools": false, "ip_address_type": "IPV4", "sync_realization": false, "resource_type": "IpAddressPool", "id": "ipp_f9fa99f4-6269-4124-a8e6-df245a9600c4_1", "display_name": "ipp-test4-1", "path": "/infra/ip-pools/ipp_f9fa99f4-6269-4124-a8e6-df245a9600c4_1", "relative_path": "ipp_f9fa99f4-6269-4124-a8e6-df245a9600c4_1", "parent_path": "/infra", "remote_path": "", "unique_id": "fd1392d5-f188-4e16-a023-15d315bb1ad3", "realization_id": "fd1392d5-f188-4e16-a023-15d315bb1ad3", "owner_id": "40cc9f04-12af-408c-aecc-b4dd44c44b4b", "_index": "nsx_policy_ipaddresspoolblocksubnet", "_type": "_doc", "_id": "/infra/ip-pools/ipp_f9fa99f4-6269-4124-a8e6-df245a9600c4_1/ip-subnets/ibs_f9fa99f4-6269-4124-a8e6-df245a9600c4_1", "_score": 1.0, "_source": { "size": 128, "subnet_size": "128", "ip_block_path": "/infra/ip-blocks/0f252ba2-86ff-4584-ae0f-0477bc3cb6ae", "auto_assign_gateway": true, "resource_type": "IpAddressPoolBlockSubnet", "id": "ibs_f9fa99f4-6269-4124-a8e6-df245a9600c4_1", "display_name": "ibs-test4-1", "path": "/infra/ip-pools/ipp_f9fa99f4-6269-4124-a8e6-df245a9600c4_1/ip-subnets/ibs_f9fa99f4-6269-4124-a8e6-df245a9600c4_1", "relative_path": "ibs_f9fa99f4-6269-4124-a8e6-df245a9600c4_1", "parent_path": "/infra/ip-pools/ipp_f9fa99f4-6269-4124-a8e6-df245a9600c4_1",
То для manager-части объекта все выглядит менее радостно. Для наглядности показываем manager-половину того же IP pool:
"_index": "nsx_manager_ippool", "_type": "_doc", "_id": "fd1392d5-f188-4e16-a023-15d315bb1ad3", "_score": 1.0, "_source": { "subnets": [ { "cidr": "169.254.0.128/25", "gateway_ip": "169.254.0.129", "dns_nameservers": [], "allocation_ranges": [ { "start": "169.254.0.130", "end": "169.254.0.254" } ] } ], "check_overlap_with_existing_pools": false, "resource_type": "IpPool", "id": "fd1392d5-f188-4e16-a023-15d315bb1ad3", "display_name": "01-test4-1", "_index": "nsx_manager_ipblocksubnet", "_type": "_doc", "_id": "b54b6c13-5300-49df-881b-f6577fab694f", "_score": 1.0, "_source": { "size": 128, "cidr": "169.254.0.128/25", "allocation_ranges": [ { "start": "169.254.0.128", "end": "169.254.0.255" } ], "block_id": "67f06508-61a8-4e75-b9a2-0b93a1337e8e", "start_ip": "", "resource_type": "IpBlockSubnet", "id": "b54b6c13-5300-49df-881b-f6577fab694f", "display_name": "b54b6c13-5300-49df-881b-f6577fab694f",
Как мы видим, связь между индексами nsx_manager_ippool и nsx_manager_ipblocksubnet существует только через CIDR, а общая связь этих частей объекта с policy осуществляется через manager._id=policy.unique_id.
Продолжение следует
После пары дней кропотливой работы нам все же удалось свести данные из всех таблиц и получить готовый csv-файл с нужными переменными.
Далее оставалось только сделать простенький скрипт для залива данных обратно:
while IFS=';' read ipp ibs ip_block ip_block_mgr ipp_mgr_id CIDR intent_uuid l_key r_key intent_version meta start_ip gw end_ip one; do echo "'{ \"stringId\": \"/infra/realized-state/enforcement-points/default/ip-blocks/$ip_block/ip-pools/$ipp/block-subnets/$ibs\"}'" > /tmp/newKey echo "'{ \"abstractPolicyRealizedResource\": { \"abstractPolicyResource\": { \"managedResource\": { \"displayName\": \"$ibs\" }, \"markedForDelete\": false, \"deleteWithParent\": false, \"locked\": false, \"isOnboarded\": false, \"internalKey\": { \"left\": \"$l_key\", \"right\": \"$r_key\" }, \"gmKey\": { \"left\": \"$l_key\", \"right\": \"$r_key\" }, \"parentPath\": \"infra/realized-state/enforcement-points/default/ip-blocks/$ip_block/ip-pools/$ipp/\" }, \"realizationObjectId\": \"$intent_uuid\", \"realizationState\": \"REALIZATION_STATE_REALIZED\", \"runtimeStatus\": \"RUNTIME_STATUS_UNINITIALIZED\", \"intentVersion\": \"$intent_version\", \"realizedVersionOnEnforcement\": \"0\", \"realizationApi\": \"/api/v1/pools/ip-subnets/$intent_uuid\" }, \"entityType\": \"REALIZED_ENTITY_TYPE_RZ_IP_ADDRESS_POOL_BLOCK_SUBNET\", \"extendedAttributes\": { \"start_ip\": { \"dataType\": \"DATA_TYPE_TYPE_STRING\", \"value\": [ \"$start_ip\" ] }, \"gateway_ip\": { \"dataType\": \"DATA_TYPE_TYPE_STRING\", \"value\": [ \"$gw\" ] }, \"cidr\": { \"dataType\": \"DATA_TYPE_TYPE_STRING\", \"value\": [ \"$CIDR\" ] }, \"end_ip\": { \"dataType\": \"DATA_TYPE_TYPE_STRING\", \"value\": [ \"$end_ip\" ] } }, \"intentPath\": [ \"/infra/ip-pools/$ipp/ip-subnets/$ibs\" ] }'" > /tmp/newVal echo "'$meta'" > /tmp/newMeta echo "Adding key for $ipp" | tee -a /tmp/restore_log cmd="java -cp /usr/share/corfu/lib/corfudb-tools-4.1.20230926154243.2130.1-shaded.jar -Djava.io.tmpdir=/image/corfu-tools/temp org.corfudb.browser.CorfuStoreBrowserEditorMain --host=10.11.12.13 --port=9000 --tlsEnabled=true --keystore=/config/cluster-manager/cluster-manager/private/keystore.jks --ks_password=/config/cluster-manager/cluster-manager/private/keystore.password --truststore=/config/cluster-manager/cluster-manager/public/truststore.jks --truststore_password=/config/cluster-manager/cluster-manager/public/truststore.password --namespace=nsx --tablename=GenericPolicyRealizedResource --operation=addRecord --keyToAdd=`cat /tmp/newKey` --valueToAdd=`cat /tmp/newVal` --metadataToAdd=`cat /tmp/newMeta`" eval $cmd | tail -8 | tee -a /tmp/restore_log done <./payload.csv
После пары часов отработки скрипта (да-да, corfudb-tools умеет заливать записи только по одной и тратит на это достаточно большое количество времени) все пулы перешли в ожидаемо «зеленое» состояние и проблема была решена.
