Хабр, привет!

Сегодня рассказываем об еще одном нашем кейсе с 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 и базы заказчика мы собрали немного информации и выяснили:

  1. Данные о реализации объектов хранятся в базе Corfu в таблице GenericPolicyRealizedResource;

  2. Данные о каждой реализации объекта типа 'RZ_IP_ADDRESS_POOL_BLOCK_SUBNET' завязаны на каждом из шести индексов (таблиц) уже из другой БД — Elastic;

  3. Каждый объект состоит из двух половин policy и manager. Несмотря на то, что VMware с релиза NSX 3.х заявляет о выводе в EoL-функционала Manager UI/API, в каждом, в том числе свежесозданном продукте версии 4.x, все еще присутствует этот функционал;   

  4. Несмотря на то что скрипт для взаимодействия с 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 умеет заливать записи только по одной и тратит на это достаточно большое количество времени) все пулы перешли в ожидаемо «зеленое» состояние и проблема была решена.

Комментарии (1)


  1. green_bag94
    17.10.2024 08:30

    А, Вы, уважаемый автор, не подумали о том, что картинка на заставке некорректная и может вызывать противоречивые чувства?