Одной из задач администрирования облачной инфраструктуры является мониторинг её компонентов: важно знать о неправильной работе облака, вовремя выявлять и исправлять ошибки конфигурации. Управлять облаком VMWare можно несколькими способами:

  • для решения несложных задач в стиле unixway пригодится утилита vcd-cli . Сложно собрать пайплайн или написать shell-скрипт

  • пишете скрипты на PowerShell? Есть модуль PowerCLI

  • привычнее писать на Python? Есть библиотека pyvcloud (vcd-cli построена как раз на её базе)

  • а можно работать с VMware Cloud Director API напрямую — это более трудоёмкий, зато и более гибкий путь. Выбор языка, библиотек и комбинаций REST-запросов целиком за вами!

В этой статье рассмотрим принципиальное решение двух практических задач через API. Примеров кода будет немного, но зато с изюминкой: добавим коду асинхронности, чтобы ускорить пачку из нескольких тысяч запросов!

Подготовка

Прежде чем погрузиться в программирование, нужно понять, как посылать запросы, какие запросы понадобятся и в каком формате удобнее получать ответ. Для отладки и подбора запросов можно использовать Postman или curl.

В корпоративной базе знаний есть пара статей про работу с нашим облаком через Postman

Как получить доступ к VMware Cloud Director через vCloud API

Изменение параметров EDGE c помощью vCloud API

Согласно официальной документации, работа с API начинается с двух запросов:

1. Узнать адреса для авторизации, версии API и уровень поддержки, выполнив простой get-запрос:

PS > curl -X GET https://vcd.cloud4y.ru/api/versions

По умолчанию ответ приходит в формате xml. Если вы предпочитаете json, просто добавьте воды заголовок

-H "Accept:application/*+json"

Пример кода
PS > curl -X GET https://vcd.cloud4y.ru/api/versions -H "Accept:application/*+json"
{
"versionInfo" : [ {
    "version" : "30.0",
    "loginUrl" : "https://vcd.cloud4y.ru/api/sessions",
    "mediaTypeMapping" : [ ],
    "any" : [ ],
    "deprecated" : true,
    "otherAttributes" : { }
}, {
    "version" : "31.0",
    "loginUrl" : "https://vcd.cloud4y.ru/api/sessions",
    "mediaTypeMapping" : [ ],
    "any" : [ ],
    "deprecated" : true,
    "otherAttributes" : { }
}, {
    "version" : "32.0",
    "loginUrl" : "https://vcd.cloud4y.ru/api/sessions",
    "mediaTypeMapping" : [ ],
    "any" : [ ],
    "deprecated" : false,
    "otherAttributes" : { }
}, {
    "version" : "33.0",
    "loginUrl" : "https://vcd.cloud4y.ru/api/sessions",
    "mediaTypeMapping" : [ ],
    "any" : [ ],
    "deprecated" : false,
    "otherAttributes" : { }
}, {
    "version" : "34.0",
    "loginUrl" : "https://vcd.cloud4y.ru/api/sessions",
    "mediaTypeMapping" : [ ],
    "any" : [ ],
    "deprecated" : false,
    "otherAttributes" : { }
}, {
    "version" : "35.0",
    "loginUrl" : "https://vcd.cloud4y.ru/api/sessions",
    "mediaTypeMapping" : [ ],
    "any" : [ ],
    "deprecated" : false,
    "otherAttributes" : { }
} ],
"schemaRoot" : "https://vcd.cloud4y.ru/api/v1.5/schema/",
"any" : [ ],
"otherAttributes" : { }
}

2. Получить временные токены для авторизации дальнейших запросов к API. Для этого выполним базовую авторизацию в облаке, в случае успеха будет открыта сессия, а заголовках ответа мы найдём нужные токены.

В этом запросе необходимо указать в заголовке одну из уже известных версий API, с которой планируется работать: Accept:application/*+json;version=35.0. Ещё нужно передать учётные данные — это закодированная в base64 строка вида Login@vOrg:Password.

Пример кода
PS > curl -X POST https://vcd.cloud4y.ru/api/sessions -H "Accept:application/*+json;version=35.0" -H "Authorization:Basic TG9naW5Adk9yZzpQYXNzd29yZA==" -I
HTTP/1.1 200 OK
Server: nginx
Date: Thu, 29 Apr 2021 09:09:31 GMT
Content-Type: application/vnd.vmware.vcloud.session+json;version=35.0
Content-Length: 4945
Connection: close
X-VMWARE-VCLOUD-REQUEST-ID: 58156abf-9f16-4082-9c42-f7f1e612be0c
X-VMWARE-VCLOUD-ACCESS-TOKEN: eugXbGciOiJSUzI1NiJ9.euJzdWIiOigXZG1pbmlzdHgXdG9yIiwiaXNzIjoiZTE5N2QwZGMtYTA1Ny00YgXlLTlkZTUtMDZlMzQxMDQ4YjgzQDlhNjI1YWUzLWJjNDEtNDc5ZS1hZWY3LTIwMDI3ODM1Yzg3ZiIsImV4cCI6MTYxOTc3MzcxMSwidmVyc2lvbiI6InZjbG91ZF8xLjAiLCJqdGkiOiJlNnFNnzBhYWU4ZTY0OWJiYNn0YTBiYjY1ODA1NjgwMSJ9.EGFg_MYPkEPOHUW-k7Dh5sg0h8BrVces3e_q7iiLZ5G8t6D3RhGb1g921qipLuHWksrSYXJxxU18icpyiUNI_uwFqz88BrCaaVag-LVsrpxRWVe3COyKDl9xBw45bmuhr1ZGRIwQr8B495fDhhaILg7yB7-PlRSTKYhn2Ratew6mdDjq57ddqg_p7oIqezkuZZQ3L-On3OHCELKhqqFZ6GzescPFii22NC9_0hh_hJvmoewgXo-S1o2E-2qY--muRJm2EWOn2wIdQg_hZtA7WjKggbQNGvWSyjL9AUTz6At-2lHuZXJoORpMt5I-9Jo9NOPPx8RVgfa8cg7O8qy8Gw
X-VMWARE-VCLOUD-TOKEN-TYPE: Bearer
x-vcloud-authorization: e2fa30aae8e649bbbc4a0bb658056801
X-VMWARE-VCLOUD-REQUEST-EXECUTION-TIME: 227
Cache-Control: no-store, must-revalidate
Vary: Accept-Encoding, User-Agent
Strict-Transport-Security: max-age=31536000
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff

На данный момент работают два способа авторизации через токены:

  1. Авторизовать запрос токеном x-vcloud-authorization

    PS > curl -X GET https://vcd.cloud4y.ru/api/query?type=edgeGateway -H "Accept:application/*+json;version=35.0" -H "x-vcloud-authorization:e2fa30aae8e649bbbc4a0bb658056801"
    годится для отладки, но не рекомендуется в "боевых" скриптах, т.к. считается устаревшим и может быть удалён в будущих версиях API
  2. Использовать для авторизации токены X-VMWARE-VCLOUD-ACCESS-TOKEN и X-VMWARE-VCLOUD-TOKEN-TYPE

    PS > curl -X GET https://vcd.cloud4y.ru/api/query?type=edgeGateway -H "Accept:application/*+json;version=35.0" -H "Authorization:Bearer eugXbGciOiJSUzI1NiJ9...9Jo9NOPPx8RVgfa8cg7O8qy8Gw"
    Это рекомендованный вендором способ

Теперь есть всё необходимое, чтобы отправлять авторизованные запросы. Перейдём к задачкам.

На первое: 'Edge Health Check'

Нужно отслеживать состояние виртуальных маршрутизаторов Edge и сигнализировать, если у кого-то оно отличается от нормального.

Здесь всё достаточно просто: нормальные — это маршрутизаторы в статусе normal в Web UI vCloud Director. Любые другие состояния нужно проверять и, при необходимости, устранять. Например, маршрутизатор может оказаться в статусе critical (Web UI) / UNREACHABLE (API) по нескольким причинам:

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

  • Произошла рассинхронизация между базами NSX и vCenter. Уровень оповещения будет выше: исправить нужно как можно скорее, выполнив Redeploy маршрутизатора.

Соответствие веб-состояний маршрутизаторов их API-аналогам указано в таблице:

Статус Web UI

Статус API

Оповещение

normal

READY

REALIZED

НЕТ

warning

FAILED_CREATION

FAILED_UNDEPLOYMENT

FAILED_REDEPLOYMENT

ДА

critical

NOT_READY

UNREACHABLE

UNKNOWN

ERROR

REALIZATION_FAILED

undefined

ДА

busy

CONFIGURING

PENDING

ДА

Поиск подходящего API запроса

Документация на 500 с лишним страниц — не самый увлекательный текст. Терпения, конечно же, не хватит. Результат хочется получить сразу, на месте, как в Web UI. Поэтому можно схитрить и подсмотреть нужный запрос вместе с параметрами в консоли разработчика браузера!

GET https://vcd.cloud4y.ru/api/query?type=edgeGateway

Это помогло локализовать нужный раздел руководства (Query Service) и подобрать необходимые параметры. Чтобы не разглашать чувствительной информации, приведём пример ответа на запрос с дополнительным фильтром.

Пример
PS > curl -X GET "https://vcd.cloud4y.ru/api/query?type=edgeGateway&filter=(gatewayStatus!=READY);(gatewayStatus!=REALIZED);(name==*mih*)" -H "Accept:application/*+json;version=35.0" -H "x-vcloud-authorization:e2fa30aae8e649bbbc4a0bb658056801"
{
  "otherAttributes" : { },
  "link" : [ {
    "otherAttributes" : { },
    "href" : "https://vcd.cloud4y.ru/api/query?type=edgeGateway&page=1&pageSize=25&format=references&filter=(gatewayStatus!=READY);(gatewayStatus!=REALIZED);(name==*mih*)",
    "id" : null,
    "name" : null,
    "type" : "application/vnd.vmware.vcloud.query.references+xml",
    "model" : null,
    "rel" : "alternate",
    "vCloudExtension" : [ ]
  }, {
    "otherAttributes" : { },
    "href" : "https://vcd.cloud4y.ru/api/query?type=edgeGateway&page=1&pageSize=25&format=references&filter=(gatewayStatus!=READY);(gatewayStatus!=REALIZED);(name==*mih*)",
    "id" : null,
    "name" : null,
    "type" : "application/vnd.vmware.vcloud.query.references+json",
    "model" : null,
    "rel" : "alternate",
    "vCloudExtension" : [ ]
  }, {
    "otherAttributes" : { },
    "href" : "https://vcd.cloud4y.ru/api/query?type=edgeGateway&page=1&pageSize=25&format=idrecords&filter=(gatewayStatus!=READY);(gatewayStatus!=REALIZED);(name==*mih*)",
    "id" : null,
    "name" : null,
    "type" : "application/vnd.vmware.vcloud.query.idrecords+xml",
    "model" : null,
    "rel" : "alternate",
    "vCloudExtension" : [ ]
  }, {
    "otherAttributes" : { },
    "href" : "https://vcd.cloud4y.ru/api/query?type=edgeGateway&page=1&pageSize=25&format=idrecords&filter=(gatewayStatus!=READY);(gatewayStatus!=REALIZED);(name==*mih*)",
    "id" : null,
    "name" : null,
    "type" : "application/vnd.vmware.vcloud.query.idrecords+json",
    "model" : null,
    "rel" : "alternate",
    "vCloudExtension" : [ ]
  } ],
  "href" : "https://vcd.cloud4y.ru/api/query?type=edgeGateway&page=1&pageSize=25&format=records&filter=(gatewayStatus!=READY);(gatewayStatus!=REALIZED);(name==*mih*)",
  "type" : "application/vnd.vmware.vcloud.query.records+json",
  "name" : "edgeGateway",
  "page" : 1,
  "pageSize" : 25,
  "total" : 2,
  "record" : [ {
    "_type" : "QueryResultEdgeGatewayRecordType",
    "link" : [ ],
    "metadata" : null,
    "href" : "https://vcd.cloud4y.ru/api/admin/edgeGateway/62e4464a-905c-4dbc-adab-2504545d9ba6",
    "id" : null,
    "type" : null,
    "otherAttributes" : {
      "task" : "https://vcd.cloud4y.ru/api/task/7b950b14-8b49-4871-a2db-640fae971c0f",
      "isSyslogServerSettingInSync" : "true",
      "taskOperation" : "nsxProxyResourceConfigureServices",
      "taskStatus" : "success",
      "taskDetails" : " "
    },
    "advancedNetworkingEnabled" : true,
    "availableNetCount" : 8,
    "distributedRoutingEnabled" : false,
    "edgeGatewayType" : "NSXV_BACKED",
    "egressPointId" : null,
    "gatewayStatus" : "UNREACHABLE",
    "haStatus" : "DISABLED",
    "isBusy" : false,
    "name" : "mihailovgpuwin2019test2_EDGE",
    "numberOfExtNetworks" : 1,
    "numberOfOrgNetworks" : 1,
    "orgName" : "mihailovgpuwin2019test2",
    "orgVdcName" : "mihailovgpuwin2019test2_VDC_hk41gpu",
    "vdc" : "https://vcd.cloud4y.ru/api/vdc/09fe3b51-c908-4e9a-a4b8-36d69a7853b8",
    "vdcGroupId" : null,
    "vdcGroupName" : null
  }, {
    "_type" : "QueryResultEdgeGatewayRecordType",
    "link" : [ ],
    "metadata" : null,
    "href" : "https://vcd.cloud4y.ru/api/admin/edgeGateway/6942a0bc-7569-49da-9581-203a402386d8",
    "id" : null,
    "type" : null,
    "otherAttributes" : {
      "task" : "https://vcd.cloud4y.ru/api/task/313b3dcc-0cf0-42ac-858d-fcea13d49ed2",
      "isSyslogServerSettingInSync" : "true",
      "taskOperation" : "networkEdgeGatewayCreate",
      "taskStatus" : "success",
      "taskDetails" : " "
    },
    "advancedNetworkingEnabled" : true,
    "availableNetCount" : 9,
    "distributedRoutingEnabled" : false,
    "edgeGatewayType" : "NSXV_BACKED",
    "egressPointId" : null,
    "gatewayStatus" : "UNREACHABLE",
    "haStatus" : "DISABLED",
    "isBusy" : false,
    "name" : "mihailov-edge-health-check-demo",
    "numberOfExtNetworks" : 1,
    "numberOfOrgNetworks" : 0,
    "orgName" : "mihailov-vorg",
    "orgVdcName" : "mihailov-vdc_HM14",
    "vdc" : "https://vcd.cloud4y.ru/api/vdc/6f5c8aaf-b5e0-4317-940b-cf22b6019229",
    "vdcGroupId" : null,
    "vdcGroupName" : null
  } ],
  "vCloudExtension" : [ ]
}

Самое интересное ждёт после секции link.

Эти параметры помогут получить все объекты (записи), удовлетворяющие нашим критериям:

"page" : 1,         // номер текущей страницы (ответа)
"pageSize" : 25,    // количество объектов на один ответ
"total" : 2,        // общее количество объектов

А здесь достаточно деталей, чтобы приготовить правильный и понятный объект мониторинга:

"record": [         // список объектов с более подробной информацией
    {
        ...
        "gatewayStatus" : "UNREACHABLE",            // то самое состояние, которое нужно контролировать
        "name" : "mihailov-edge-health-check-demo", // имена маршрутизатора и элементов клиентской
        "orgName" : "mihailov-vorg",                // инфраструктуры: виртуальной организации
        "orgVdcName" : "mihailov-vdc_HM14",         // и виртуального дата-центра
        ...
    }
]

Итак, определены необходимые запросы. Полная реализация решения в коде потребует дополнительных усилий для многих системных администраторов (они ведь далеко не разработчики), но вполне тривиальна и не представляет особого интереса: Python3 + библиотека requests.

Оптимизировать тут тоже особо нечего, в реальности число объектов колеблется от 0 до нескольких штук и вся информация собирается за один приём. Если в вашем случае число объектов порядка нескольких сотен, то можно задать максимально допустимое значение в параметре "pageSize" : 128, чтобы собирать полный список за два-три запроса.

На второе: 'Disk Provisioning vs Storage Profile'

Находить несоответствия между типами дисков ВМ и профилями СХД.

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

Во-вторых, есть несколько способов предоставления места под файлы дисков ВМ:

  • Thin Provisioning. "Тонкие" диски позволяют сэкономить дисковое пространство, т.к. занимают ровно столько места, сколько использует гостевая ОС. Например, в свойствах ВМ вы с запасом указали 200 ГБ, фактически же используется 50 ГБ, поэтому файл диска виртуальной машины будет занимать всего 50 из выделенных 200 ГБ. Но зато это снижает производительность диска ВМ, т.к. появляются накладные расходы при изменении фактического размера диска.

  • Thick Provisioning. "Толстые" диски наоборот, более производительны, так как сразу выделяется весь запрошенный объём и он не скачет от текущей фактической потребности гостевой ОС, зато это приводит к излишнему перерасходу места на томах СХД.

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

Подбор нужных запросов

На этот раз придётся глубже залезть в документацию, потому что нельзя вот так просто взять и собрать всю инфу за один запрос к API. Да, можно использоватьQuery Service, чтобы собрать полный список виртуальных машин. Но в нём будет только общая информация: имена / id VM, vOrg, vDC — без конкретики по дискам. Конфигурация виртуальной машины запрашивается адресно, отдельно по каждой ВМ, но уровень детализации очень подробный. В процессе отладки был момент, когда оба списка были сохранены в файлы, а потом их сравнили. Получилось 3+ МБ общий против 120+ МБ детальный.

Схема запросов в общих чертах:

1. Общая информация по виртуальным машинам

PS > curl -X GET "https://vcd.cloud4y.ru/api/query?type=adminVM" -H "Accept:application/*+json;version=35.0" -H "x-vcloud-authorization:e2fa30aae8e649bbbc4a0bb658056801"
Ответ будет примерно таким:
{
    ...
    "name": "adminVM",
    "page": 1,
    "pageSize": 128,
    "total": 999,
    "record": [
        ...
        {
            "_type": "QueryResultAdminVMRecordType",
            "href": "https://vcd.cloud4y.ru/api/vApp/vm-8ed1331e-23b2-43b3-a869-6d324561d188",  // прямая ссылка для запроса конфигурации ВМ
            "containerName": "Ubuntu-20.04_Template",
            "dateCreated": "2020-12-14T08:44:44.214Z",
            "description": "шаблон ВМ",
            "guestOs": "Ubuntu Linux (64-bit)",
            "name": "base for template ubuntu-20.04",
            "org": "https://vcd.cloud4y.ru/api/org/e197b8dc-a357-4d8e-9de9-06e341348b83",   // по ID можно узнать имя
            "status": "POWERED_OFF",
            "storageProfileName": "vcd-type-med",
            "vdc": "https://vcd.cloud4y.ru/api/vdc/6f5c8aaf-b5e0-4317-940b-cf22b6019229",   // по ID можно узнать имя
            "vmToolsVersion": 11301
        },
        ...

Ориентируемся на total, в цикле собираем остальные страницы. В итоге получим список с именами ВМ, их прямыми ссылками и некоторой дополнительной информацией.

2. Детальная информация

PS > curl -X GET "https://vcd.cloud4y.ru/api/vApp/vm-8ed1331e-23b2-43b3-a869-6d324561d188" -H "Accept:application/*+json;version=35.0" -H "x-vcloud-authorization:e2fa30aae8e649bbbc4a0bb658056801"
Искомые свойства дисков находятся в секции спецификации:
{
    ...
    "section": [
        {
            "_type": "VmSpecSectionType",
            ...
            "diskSection": {
                "otherAttributes": {},
                "diskSettings": [
                    {
                        "otherAttributes": {},
                        "diskId": "2016",
                        "sizeMb": 10240,
                        "unitNumber": 0,
                        "busNumber": 1,
                        "adapterType": "4",
                        "thinProvisioned": true,    // тип диска
                        "disk": null,
                        "storageProfile": {
                            ...
                            "name": "vcd-type-max", // профиль хранилища
                            "type": "application/vnd.vmware.vcloud.vdcStorageProfile+xml",
                        },
                    }
                ],
            },
        }

3. Хотя этого достаточно, вся необходимая информация получена, но в мониторинг будут смотреть люди и хотелось бы видеть имена виртуальных организации и дата-центра клиента (vOrg & vDC) вместо ID-ссылок.

Все тот же Query Service, из нового - параметр format=references - получаем минимум подробностей, в отличие от records.

Организации:
PS > curl -X GET "https://vcd.cloud4y.ru/api/query?format=references&type=organization" -H "Accept:application/*+json;version=35.0" -H "x-vcloud-authorization:e2fa30aae8e649bbbc4a0bb658056801"
{
    ...
    "reference": [
        {
            "otherAttributes": {},
            "href": "https://vcd.cloud4y.ru/api/org/e197b8dc-a357-4d8e-9de9-06e341348b83",
            "id": "urn:vcloud:org:e197b8dc-a357-4d8e-9de9-06e341348b83",
            "name": "mihailov-vorg",
            "type": "application/vnd.vmware.vcloud.org+xml",
            "vCloudExtension": []
        },
        ...
Дата-центры:
PS > curl -X GET "https://vcd.cloud4y.ru/api/query?format=references&type=adminOrgVdc" -H "Accept:application/*+json;version=35.0" -H "x-vcloud-authorization:e2fa30aae8e649bbbc4a0bb658056801"
{
    ...
    "reference": [
        {
            "otherAttributes": {},
            "href": "https://vcd.cloud4y.ru/api/admin/vdc/6f5c8aaf-b5e0-4317-940b-cf22b6019229",
            "id": "urn:vcloud:vdc:6f5c8aaf-b5e0-4317-940b-cf22b6019229",
            "name": "mihailov-vdc_HM14",
            "type": "application/vnd.vmware.admin.vdc+xml",
            "vCloudExtension": []
        },
        ...

На десерт: решение проблемы с вариантом "в лоб"

Кто виноват?

Классический подход с requests является последовательным: запросили первый объект — получили первый. Запросили второй — получили второй и т.д. Проблема заключается в линейной зависимости: чем больше объектов, тем дольше цикл.

Запросы Query Service можно немного подкрутить через API: запрашивать по 128 объектов за раз вместо 25 по умолчанию, — и слегка ускорить процесс. Но вторая страница всё равно будет запрошена только после получения первой. Сбор же детальной информации никак не подкрутишь. Когда в облаке несколько тысяч виртуальных машин, процесс занимает десятки минут или даже часы, в зависимости от текущей нагрузки на облако.

Реальный пример из лога, 58 минут.
2021-04-02 23:16:14,063 | vam-py script     | INFO     |                  vam.py |                    main: 105 | ================================================================================
2021-04-02 23:16:14,063 | vam-py script     | DEBUG    |                  vam.py |                    main: 106 | Namespace(auth_probe=False, check_json=False, disk_info=True, edge_info=False)
2021-04-02 23:16:14,063 | vam-py script     | DEBUG    |                  vam.py |                    main: 143 | ['disk_info']
2021-04-02 23:16:14,063 | vam-py script     | DEBUG    |               helper.py |           update_tokens: 434 | running

...

2021-04-03 00:14:12,828 | vam-py script     | DEBUG    |               helper.py |  write_list_to_csv_file: 463 | running
2021-04-03 00:14:12,828 | vam-py script     | DEBUG    |               helper.py |  get_current_script_dir:  23 | running
2021-04-03 00:14:12,828 | vam-py script     | DEBUG    |               helper.py |  get_current_script_dir:  32 | done
2021-04-03 00:14:12,843 | vam-py script     | DEBUG    |               helper.py |  write_list_to_csv_file: 478 | done
2021-04-03 00:14:12,844 | vam-py script     | INFO     |          helper_disk.py |                     foo:  83 | disk-status.csv report ready

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

С другой стороны, отладка — это ручной процесс, запускается много раз. Становится очень критично, сколько времени отнимают отдельные операции.

Что делать?

Пора учится новым трюкам. Если заменитьrequests на aiohttp + asyncio , то получим x10 - x15 boost: время подготовки данных сократилось до 3-5 минут! Отлаживать скрипт стало намного быстрее, а бонусом — быстрее собрали грабли сбоев и улучшили код.

Что же произошло? Количество объектов в облаке, т.е. объём запросов, осталось плюс-минус прежним. Но теперь скрипт посылает запросы один за другим, не дожидаясь ответа на предыдущий запрос, ответы собираются по мере готовности.

Работает это примерно так: по общему списку ВМ запустили пачку детальных запросов, собрали и сохранили успешные ответы. Сбойные запросы повторяем с небольшой задержкой между ними (80..100 мс). Это зелье против 500-х ошибок. Бывает и так, что с момента запуска ВМ успели удалить, тогда будут 400-е ошибки. В таком случае повторитель, следуя принципу Эйнштейна — делать одно и то же снова и снова и ожидать иного результата глупо, — перестаёт зазря долбить запросами и возвращает успешные ответы.

Немного обещанного исходного кода спрятано под катом.

Вспомогательный модуль, в т.ч. асинхронные функции.
# -*- coding: utf-8 -*-

import json
import sys
import aiohttp
import asyncio

# my custom modules
from conf import config as cfg_py
from helper_log import log
import helper


# асинхронно выполняет неблокирующий запрос
async def task(session, url, params=None) -> dict:
    """
    асинхронно выполняет неблокирующий запрос

    :type session:  aiohttp.client.ClientSession
    :param session: сессия, в которой нужно выполнить запрос

    :type url:      str
    :param url:     адрес для выполнения запроса

    :type params:   dict
    :param params:  параметры запроса

    :return:        ответ сервера в случае успеха или параметры неудачного запроса
    """
    response = {}
    msg = '555 FAIL '
    try:
        async with session.get(url, params=params) as resp:
            if 200 == resp.status:
                response = await resp.json()
                msg = f"{resp.status}  OK  "
            else:
                response[resp.status] = (url, params)  # вернём параметры неудачного запроса для его повтора
                msg = f"{resp.status} FAIL "

    except BaseException as e:
        response[555] = (url, params)  # 555, т.к. далее ошибочными считаются все коды из диапазона range(400, 600)
        log.critical(f"{e}")

    finally:
        log.debug(f"{msg}{session} url {url} params {params}")
        return response


# асинхронно запускает задания в работу в одной сессии
async def task_launcher(url_params, headers, delay=0) -> list:
    """
    асинхронно запускает задания в работу в одной сессии

    :type url_params:   list
    :param url_params:  список кортежей (url, params) - адреса и их параметры, так сделано для переиспользования функции
                        в других местах: некоторые запросы выполняются без параметров по уникальному адресу объекта

    :type headers:      dict
    :param headers:     заголовки сессии: указание вернуть ответ в json, токены авторизации

    :type delay:        int
    :param delay:       задержка для повторных массовых запросов. Если на некоторые запросы вернулась ошибка,
                        можно повторно запустить их в работу, указав задержку.
                        Настраивается в conf.py -> config['AIO_DELAY']

    :return:            список результатов выполнения заданий: ответов по каждому (и успешному, и неудачному) запросу
    """
    response = []
    try:
        log.info(f"{len(url_params)} async request(s) running")

        async with aiohttp.ClientSession(headers=headers) as session:
            tasks = []
            for tpl in url_params:
                url, params = tpl
                tasks.append(
                    asyncio.ensure_future(
                        task(session, url, params=params)
                    )
                )
                await asyncio.sleep(delay)  # задержка после каждого задания, можно использовать в повторных вызовах
            response = await asyncio.gather(*tasks)

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        # # задания провалены, если ключи из множества ошибочных кодов
        # failed_tasks = [t.result() for t in tasks if t.result().keys() & set(range(400, 600))]
        # msg = f"finished with {len(failed_tasks)} fails" if len(failed_tasks) > 0 else "ok"
        # log.debug(f"{msg}")
        return response


# формирует заголовки для сессии запросов: токен-авторизацию, указание вернуть ответ в json
def get_session_headers(vc) -> dict:
    """
    формирует заголовки для сессии запросов: токен-авторизацию, указание вернуть ответ в json

    :type vc:   dict
    :param vc:  облако из json-конфига, ожидается, что оно содержит актуальные токены, см. helper.py -> update_tokens()

    :return:    словарь с заголовками авторизации и ожидаемым форматом ответа
    """
    headers = {}
    try:
        headers = {"Accept": f"application/*+json;version={vc['api_ver']};multisite=global"}

        # проверка способов авторизации по токенам
        # проверка JSON Web Token
        if len(vc.get('X-VMWARE-VCLOUD-ACCESS-TOKEN', '')) > 0                 and len(vc.get('X-VMWARE-VCLOUD-TOKEN-TYPE', '')) > 0:
            headers['Authorization'] = f"{vc['X-VMWARE-VCLOUD-TOKEN-TYPE']} "                                        f"{vc['X-VMWARE-VCLOUD-ACCESS-TOKEN']}"
            log.info(f"JSON Web Token exist for {vc['url']}")

        # проверка legacy токена
        elif len(vc.get('x-vcloud-authorization', '')) > 0:
            headers['x-vcloud-authorization'] = f"{vc['x-vcloud-authorization']}"
            log.info(f"LEGACY token exist for {vc['url']}")

        # нет доступных вариантов авторизации для этого vCloud`а
        else:
            log.warning(f"NO AUTH TOKEN, skipped {vc['url']}")

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        log.debug(f"headers {headers['Accept']} ")
        return headers


# формирует словарик с параметрами запроса
def get_query_params(q_type, q_format=None, q_filter='', page=1, page_size=cfg_py['PAGE_SIZE']) -> dict:
    """
    формирует словарик с параметрами запроса

    :type q_type:       str
    :param q_type:      тип запроса: 'edgeGateway', 'adminVM', 'organization', ...

    :type q_format:     str
    :param q_format:    формат записей: 'idrecords' | 'records' | 'references'
                        # todo вот тут нужно проверить (напр. запросом аплинков): если формат не указан,
                            то как правильно сделать? None, '' или в параметрах вообще не должно быть элемента?!

    :type q_filter:     str
    :param q_filter:    фильтр объектов
                        # todo параметр вызывает сбой, нужно разобраться,
                            как передавать фильтр вида 'org==*demo-*'
                            и как его кодировать

    :type page:         int
    :param page:        страница для запроса

    :type page_size:    int
    :param page_size:   количество записей на одной странице

    :rtype:             dict
    :return:            словарь с параметрами
    """
    params = {
        'type':     q_type,
        'format':   q_format or '',
        'page':     page,
        'pageSize': page_size,
        # # 'filter': 'vdc==urn:vcloud:vdc:6f5c8aaf-b5e0-4317-940b-cf22b6019229',
        # # 'filterEncoded': True,
        'filter': q_filter or '',
        # 'links':    'false',
        # 'sortAsc':  'name',
    }
    log.debug(f"type='{q_type}' format='{q_format}' filter='{q_filter}' page={page} page_size={page_size}")
    return params


# отправляет список запросов в работу, повторяет неудачные запросы в цикле с задержкой
def repeater(url_params, headers) -> list:
    """
    отправляет список запросов в работу, повторяет неудачные запросы в цикле с задержкой

    :type url_params:   list
    :param url_params:  ответ с первой страницей

    :type headers:      dict
    :param headers:     заголовки сессии: проксируем далее в task_launcher

    :return:            успешно полученные записи
    """
    ret = []
    ws = 1  # контроль выхода из цикла повторов при превышении порогового значения (conf.py -> conf['WS'])
    wsl = [sys.maxsize, sys.maxsize - 1]  # 3им элементом будет реальное кол-во неудачных запросов после 1го запуска,
    # тогда условие цикла (wsl[-1] < wsl[-2] <= wsl[-3] or wsl[-1] <= wsl[-2] < wsl[-3]) гарантирует выход после
    # 3х прогонов цикла, если кол-во неудачных запросов не уменьшается в результате повторов

    try:
        # 1й запуск пачки запросов без задержки
        response = asyncio.run(task_launcher(url_params=url_params, headers=headers, delay=0))

        fail = [r for r in response if r.keys() & set(range(400, 600))]
        ok_r = [r for r in response if not r.keys() & set(range(400, 600))]

        if len(response) != len(ok_r) + len(fail):
            log.warning(f"проблема фильтрации ok vs fail: total {len(response)} != {len(ok_r) + len(fail)} "
                        f"сумме ok {len(ok_r)} + fail {len(fail)}")
        ret += ok_r

        # повтор с задержкой всех неудачных запросов
        wsl.append(len(fail))
        # todo 'И кол-во зафейленных НЕ уменьшается за последнии 3 прогона цикла'
        while len(fail) > 0 and (wsl[-1] < wsl[-2] <= wsl[-3] or wsl[-1] <= wsl[-2] < wsl[-3]) and ws < cfg_py['WS']:
            log.warning(f"{ws} repeat loop, delay {cfg_py['AIO_DELAY']}, previously failed {len(fail)}")
            ws += 1
            # достаём параметры неудачных запросов из [{code: (url1, params1)}, {code: (url1, params1)}, ...]
            url_params = [v for r in fail for k, v in r.items()]
            # повторяем с задержкой
            response = asyncio.run(task_launcher(url_params=url_params, headers=headers, delay=cfg_py['AIO_DELAY']))
            fail = [r for r in response if r.keys() & set(range(400, 600))]
            ret += [r for r in response if not r.keys() & set(range(400, 600))]
            wsl.append(len(fail))

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        fails_chain = ' > '.join([f"{f}" for f in wsl[2::]])
        log.info(f"{len(ret)} object(s) returned after {len(wsl[2::])} run(s), fails_chain {fails_chain}")
        return ret


# выбирает записи (значения ключей) из ответов (страниц со списками словарей)
def get_elements_by_key(lst, key_name) -> list:
    """
    выбирает из списка lst нужные секции key_name

    :type lst:          list
    :param lst:         список со страницами ответов

    :type key_name:     str
    :param key_name:    имя секции: 'record', 'section', 'reference'

    :return:            список содержимого секций 'record'
    """

    values_lst = []

    try:
        for el in lst:
            if el is not None and type(el) == dict:
                values_lst += el.get(key_name, [])

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        log.debug(f"{len(values_lst)} {key_name}(s) extracted from {len(lst)} page(s)")
        return values_lst


# получает объекты указанного типа со всех облаков json-конфига
def query_objects(q_type, q_format='', extract_key='record', q_filter='', page_size=cfg_py['PAGE_SIZE']) -> list:
    """
    получает объекты указанного типа со всех облаков json-конфига

    :type q_type:       str
    :param q_type:      тип запрашиваемого объекта: adminVM | organization | adminOrgVdc | ...

    :type q_format:     str
    :param q_format:    формат записей об объектах: records | idrecords | references

    :type extract_key:  str
    :param extract_key: название секции записей, которую нужно извлечь (для передачи вспомогательной функции):
                        record | reference | section

    :type q_filter:     str
    :param q_filter:    фильтр объектов

    :type page_size:    int
    :param page_size:   максимальное количество объектов на один ответ

    :return:            полный список объектов
    """
    all_vc_records = []

    try:
        log.info(f"query type '{q_type}' format '{q_format}' extract key '{extract_key}' filter '{q_filter}' page size = {page_size}")
        # bug fix 'RuntimeError: Event loop is closed'
        # либо использовать python 3.9 # https://github.com/aio-libs/aiohttp/issues/4324#issuecomment-805851060
        # либо для python 3.8 на OS Windows # https://github.com/encode/httpx/issues/914#issuecomment-622586610
        if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'):
            asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

        cfg_js = json.loads(helper.read_file(cfg_py['CONF_JSON']))
        for vc in cfg_js['vClouds']:
            pages = []  # сырые ответы по каждому облаку
            url = vc['url'] + vc.get('api_query', '/api/query')
            headers = get_session_headers(vc)  # заголовки авторизации для облака
            params = get_query_params(q_type=q_type, q_format=q_format, q_filter=q_filter, page_size=page_size)
            url_params = [(url, params)]

            # запрос первой страницы, по которой можно определить, сколько ещё страниц нужно получить
            resp = asyncio.run(task_launcher(url_params=url_params, headers=headers))  # первая страница

            # проверка успешности первого запроса
            failed_r = [r for r in resp if r.keys() & set(range(400, 600))]
            if len(resp) > 0 and len(failed_r) != 0:
                log.error(f"1st page FAILED {failed_r[0]}")
                continue

            log.info(f"1st page OK: {resp[0]['total']} records expected")

            # добавляем 1й ответ к списку сырых ответов
            pages += resp

            # и проанализируем 1ю страницу: нужны ли ещё следующие страницы
            page, page_size, total = resp[0]['page'], resp[0]['pageSize'], resp[0]['total']
            if total <= page * page_size:
                all_vc_records += get_elements_by_key(pages, key_name=extract_key)
                continue  # если все записи получены уже на 1й странице - переходим к сл. облаку

            # нужно получить больше страниц: формируем пары (url, params) и передаём их в repeater
            url_params = []
            for i in range(2, ((total // page_size + int(total % page_size != 0)) + 1)):
                params = get_query_params(q_type=q_type, q_format=q_format, q_filter=q_filter,
                                          page=i, page_size=page_size)
                url_params.append((url, params))

            resp = repeater(url_params=url_params, headers=headers)
            pages += resp

            # обработка ответов, в итоге нужен только ключ 'record' - списки записей
            records = get_elements_by_key(pages, key_name=extract_key)
            all_vc_records += records

            # важен порядок ... И ... т.к. если resp пуст, то resp[0]['total'] даст исключение индексации
            # if len(pages) > 0 and len(resp) > 0 and len(records) == total:  # pages это страницы с records - записями
            if len(records) == total:  # pages это страницы с records - записями
                log.info(f"{len(records)} {extract_key}s received from {vc['url']}")
            else:
                log.warning(f"FAIL only {len(records)} of {total} from {vc['url']}")

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        log.info(f"{len(all_vc_records)} {extract_key}s received")
        return all_vc_records


# получает полную информацию об объектах из списка, используя прямые api-ссылки объектов
def get_objects_details(objects) -> list:
    """
    получает полную информацию об объектах из списка, используя прямые api-ссылки объектов (напр. виртуальных машин)

    :type objects:  list
    :param objects: список объектов, у каждого должен быть ключ 'href' - ссылка прямого доступа по API

    :return:        список объектов с детальной информацией
    """
    ret = []
    vc_dict = {}

    try:
        log.debug(f"{len(objects)} objects")
        cfg_js = json.loads(helper.read_file(cfg_py['CONF_JSON']))
        all_headers = {vc['url']: get_session_headers(vc) for vc in cfg_js['vClouds']}
        for obj in objects:
            vc = [vc for vc in cfg_js['vClouds'] if vc['url'] == obj['otherAttributes']['site']]
            if 0 == len(vc):
                log.warning(f"нифига, проблема с атрибутами ВМ {obj.get('name', None)}, не удалось определить облако")
                continue

            # т.к. список объектов общий для всех облаков конфига, то мастерим словарик
            # {'облако': {'заголовок': '{...}', 'url_params': [(url1, params1), (url2, params2), ...]}, ...}
            # по нему для каждого облака в рамках своей сессии будет своя пачка запросов детальной инфы объекта
            vc = vc[0]
            vc_dict[vc['url']] = vc_dict.get(vc['url'], {})
            vc_dict[vc['url']]['headers'] = vc_dict[vc['url']].get('headers', all_headers[vc['url']])
            vc_dict[vc['url']]['url_params'] = vc_dict[vc['url']].get('url_params', [])
            vc_dict[vc['url']]['url_params'].append((obj['href'], None))

        # отправляем пачки запросов - каждую своему облаку
        for vc, dct in vc_dict.items():
            # todo простор для оптимизации: можно же асинхронно опрашивать облака :)
            resp = repeater(url_params=dct['url_params'], headers=dct['headers'])
            ret += resp

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        return ret


# добавляет имена vOrg и vDC по их ID к свойствам виртуальной машины из списка ВМ
def vm_join_org_vdc_names(vms, orgs, vdcs) -> list:
    """
    добавляет имена vOrg и vDC по их ID к свойствам виртуальной машины из списка ВМ

    :type vms:      list
    :param vms:     исходный список виртуальных машин, где каждая запись содержит ID vOrg и vDC.
                    получить такой список можно через api-сервис запросов 'https://.../api/query?type=adminVM&format=...
                    где формат format=records или format=idrecords

    :type orgs:     list
    :param orgs:    список виртуальных организаций

    :type vdcs:     list
    :param vdcs:    список виртуальных датацентров

    :return:
    """
    ret = []
    try:
        log.debug(f"{len(vms)} vms, {len(orgs)} orgs, {len(vdcs)} vdcs")
        ret = vms.copy()

        # vm + org name
        for o in orgs:
            id_obj = ''
            if o['id'] is not None:
                id_obj = o['id'].split(':')[-1]
            elif o['href'] is not None:
                id_obj = o['href'].split(':')[-1]
            for v in vms:
                if id_obj in v.get('org', ''):
                    v['org_name'] = o['name']

        # vm + vdc name
        for d in vdcs:
            id_obj = ''
            if d['id'] is not None:
                id_obj = d['id'].split(':')[-1]
            elif d['href'] is not None:
                id_obj = d['href'].split(':')[-1]
            for v in vms:
                if id_obj in v.get('vdc', ''):
                    v['vdc_name'] = d['name']

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        log.info(f"{len(vms)} in -> {len(ret)} out")
        return ret


# вытаскивает из спецификации виртуальной машины секцию с дисками для каждой ВМ из списка
def get_vm_disks_section(details) -> dict:
    """
    вытаскивает из спецификации виртуальной машины секцию с дисками для каждой ВМ из списка

    :type details:  list
    :param details: список виртуальных машин с полной информацией о них

    :return:        словарь {vm_id1: [disk1, disk2, ...], ...}
    """
    ret = {}

    try:
        log.debug(f"{len(details)} details")

        # из всей детальной инфы нужно оставить id ВМ (чтобы позже добавить её имя и проч.) и все её диски
        # section -> словарь где есть пара "_type": "VmSpecSectionType" -> значение ключа diskSection (None или [...])

        sections = {d['id'].split(':')[-1]: d.get('section', {}) for d in details if d is not None}

        vm_specs = {vmid: s for vmid, ss in sections.items() for s in ss
                    if s is not None and s.get('_type', None) == 'VmSpecSectionType'}
        disk_sections = {vmid: spec.get('diskSection', {}).get('diskSettings', []) for vmid, spec in vm_specs.items()
                         if spec.get('diskSection', {}) is not None}
        ret = disk_sections

        # какие были ошибки и как их отлаживать
        #   1 - ошибки не было, но когда вылетела чанга EX2, её не было в vm_specs из-за "status": "UNRESOLVED"
        #   sections.keys() - vm_specs.keys()  # {'dd6c5f3f-dfab-47e8-a008-6f6bb8e6d624'}
        #   #
        #   2 - встречаются шаблоны, у которых не только диск, но и др. разделы (cpu, memory) == None
        #   вот как найти такие ВМ
        #   fails = {vmid for vmid, spec in vm_specs.items() if type(spec.get('diskSection', {})) != dict}
        #   и напечатать выражение-фильтр для ручного запроса, через curl | Postman
        #   f"filter:{','.join([f'id=={s}' for s in fails])}"  # печатает фильтр для ручного запроса в Postman

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        log.info(f"return {len(ret)} VM(s), {sum([len(v) for k, v in ret.items()])} disk(s)")
        return ret


# формирует список дисков виртуальных машин, комбинируя базовые данные по ВМ и спецификацию её дисков
def gather_disks_info(basics, details) -> list:
    """
    формирует список дисков виртуальных машин, комбинируя базовые данные по ВМ и спецификацию её дисков
    и дополняет его именами vOrg / vDC / VM

    :type basics:   list
    :param basics:  список виртуальных машин

    :type details:  dict
    :param details: словарь с дисками ВМ {vm_id1: [disk1, disk2, ...], ...}

    :return:        список дисков ВМ с данными про его vOrg / vDC / VM
    """
    ret = []
    thin_max, thick_not_max = 0, 0
    try:
        log.debug(f"{len(basics)} VM(s) basics, {len(details)} VM(s) details")
        for vmid, disks in details.items():
            obj = [vm for vm in basics if vmid in vm.get('href', '')] or [{}]
            obj = obj.pop()
            for disk in disks:
                # расхождения thin_provisioned vs storage_profile
                if disk.get('thinProvisioned', None)                         and disk.get('storageProfile', {}).get('name', None) == 'vcd-type-max':
                    trigger_level, thin_max = 1, thin_max + 1

                elif not disk.get('thinProvisioned', None)                         and disk.get('storageProfile', {}).get('name', None) != 'vcd-type-max':
                    trigger_level, thick_not_max = 2, thick_not_max + 1

                else:
                    trigger_level = 0

                d = {
                    'org_name':         obj.get('org_name', None),
                    'vdc_name':         obj.get('vdc_name', None),
                    'vm_name':          obj.get('name', None),
                    'disk_id':          disk.get('diskId', None),
                    'size_MB':          disk.get('sizeMb', -1),
                    'thin_provisioned': disk.get('thinProvisioned', None),
                    'storage_profile':  disk.get('storageProfile', {}).get('name', None),
                    'trigger_level':    trigger_level,
                    'vm_vc_id':         obj.get('moref', None),
                    'vm_vc_name':       obj.get('vmNameInVc', None),
                    'vm_id':            vmid,
                    # 'vm_api_url':       obj.get('href', None),
                    # 'vm_vdc_url':       obj.get('vdc', None),
                    # 'vm_org_url':       obj.get('org', None),
                }

                ret.append(d)
                # НЕ ПОДДЕРЖИВАЕТСЯ В API
                #  ещё бы datastore узнать: при ручном добавлении к ВМ диска через vCenter vCloud не проверяет
                #  datastore файла диска, а лепит к нему ту storage_policy, что стоит у ВМ по умолчанию.
                #  Поэтому, vCloud может отражать некорректные свойства диска
                #  Например, после ручного добавления диска забыли в Director`е назначить ему storage_policy:

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        log.info(f"{len(ret)} disk(s) total, thin_max {thin_max}, thick_not_max {thick_not_max}")
        return ret


# формирует файл обнаружения данных для Zabbix
def zabbix_disks_discovery(data: list) -> dict:
    zbx = {'data': []}
    try:
        for d in data:
            e = {
                # for zabbix data element prototypes
                '{#DISK_ORG}':      d.setdefault('org_name', None),
                '{#DISK_VDC}':      d.setdefault('vdc_name', None),
                '{#DISK_VM}':       d.setdefault('vm_name', None),
                '{#DISK_ID}':       d.setdefault('disk_id', None),
                '{#DISK_SIZE_MB}':  d.setdefault('size_MB', None),
                '{#DISK_IS_THIN}':  d.setdefault('thin_provisioned', None),
                '{#DISK_STORAGE}':  d.setdefault('storage_profile', None),
                '{#DISK_TRIGGER}':  d.setdefault('trigger_level', None),  # 0 - ок, 1 - тонкий макс, 2 - толстый немакс
            }
            zbx['data'].append(e)

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        log.info(f"zabbix discovery: {len(zbx['data'])} records")
        return zbx


# формирует файл данных для Zabbix
def zabbix_disks_data(data: list) -> dict:
    zbx = {'total': len(data)}
    try:
        for d in data:
            e = {
                # for zabbix data element prototypes
                '{org_name}':           d.setdefault('org_name', None),
                '{vdc_name}':           d.setdefault('vdc_name', None),
                '{vm_name}':            d.setdefault('vm_name', None),
                '{disk_id}':            d.setdefault('disk_id', None),
                '{size_MB}':            d.setdefault('size_MB', None),
                '{thin_provisioned}':   d.setdefault('thin_provisioned', None),
                '{storage_profile}':    d.setdefault('storage_profile', None),
                '{trigger_level}':      d.setdefault('trigger_level', None),  # 0 ок, 1 тонкий макс, 2 толстый немакс
                '{vm_vc_name}':         d.setdefault('vm_vc_name', None),
                '{vm_vc_id}':           d.setdefault('vm_vc_id', None),
            }
            vm_id = d.get('vm_id', -1)
            zbx.setdefault(vm_id, [])
            zbx[vm_id].append(e)

    except BaseException as e:
        log.critical(f"{e}")

    finally:
        log.info(f"zabbix data: {len([k for k, v in zbx.items() if k != 'total'])} VM(s)"
                 f" {sum([len(v) for k, v in zbx.items() if type(v) == list])} disk(s)")
        return zbx
Часть основного модуля, отвечающая за эту задачу.
     elif args.disk_info:
            log.debug(f"{[k for k, v in args.__dict__.items() if True == v]}")
            if helper.update_tokens():
                log.info(f"auth token ready")

                # список vOrg, чтобы узнать имя vOrg по её id в списке vm
                orgs = helper_disk.query_objects(q_type='organization', q_format='references', extract_key='reference')
                helper.write_list_to_csv_file(os.path.join(cfg_py['REPORT_DIR'],
                                                           cfg_py['DISK_REPORT_CSV_ORG']), data=orgs)

                # список vDC, чтобы узнать имя vDC по его id в списке vm
                vdcs = helper_disk.query_objects(q_type='adminOrgVdc', q_format='references', extract_key='reference')
                helper.write_list_to_csv_file(os.path.join(cfg_py['REPORT_DIR'],
                                                           cfg_py['DISK_REPORT_CSV_VDC']), data=vdcs)

                # список ВМ, формат только 'records' !!!
                # иначе или не будет ссылок для прямого запроса спецификации ВМ или не будет ИД организаций и вДЦ
                vms_basic = helper_disk.query_objects(q_type='adminVM', q_format='records', extract_key='record')

                # добавляем имена организаций и вДЦ
                vms_plus = helper_disk.vm_join_org_vdc_names(vms_basic, orgs=orgs, vdcs=vdcs)
                helper.write_list_to_csv_file(os.path.join(cfg_py['REPORT_DIR'],
                                                           cfg_py['DISK_REPORT_CSV_VM']), data=vms_plus)

                # todo TEST WHILE LOOP BY FAKES VM URL
                #  vms_basic.insert(-1, vms_basic[-1].copy())
                #  vms_basic.insert(-1, vms_basic[-1].copy())
                #  vms_basic[-2]['href'] = 'https://vcdfz.cloud4y.ru/api/vApp/vm-5752b712-db82-4399-ada8'
                #  vms_basic[-1]['href'] = 'https://vcdfz.cloud4y.ru/api/vApp/vm-02a33721-fe5e-4606-b02a'
                vms_basic.insert(0, vms_basic[0].copy())
                vms_basic[0]['href'] = vms_basic[0]['href'] + '-------------'

                # получаем полную информацию по каждой ВМ из списка (ДОЛГО!)
                vms_details = helper_disk.get_objects_details(objects=vms_basic)
                # вытаскиваем diskSection
                vms_disks = helper_disk.get_vm_disks_section(details=vms_details)
                # формируем список дисков с именами VM / vOrg / vDC
                disks = helper_disk.gather_disks_info(basics=vms_plus, details=vms_disks)
                helper.write_list_to_csv_file(os.path.join(cfg_py['REPORT_DIR'],
                                                           cfg_py['DISK_REPORT_CSV_ALL']), data=disks)

                # вспомогательные файлы
                misconfigurations = [d for d in disks if d['trigger_level'] > 0]
                helper.write_list_to_csv_file(
                    os.path.join(cfg_py['REPORT_DIR'], cfg_py['DISK_REPORT_CSV_ZBX']), data=misconfigurations)

                # zabbix файлы
                zbx_discover = helper_disk.zabbix_disks_discovery(data=misconfigurations)
                zbx_discover_result = helper.write_file(os.path.join(
                    cfg_py['REPORT_DIR'], cfg_py['DISK_REPORT_ZBX_DISCOVER']), json.dumps(zbx_discover, indent=4))

                zbx_data = helper_disk.zabbix_disks_data(data=misconfigurations)
                zbx_data_result = helper.write_file(os.path.join(
                    cfg_py['REPORT_DIR'], cfg_py['DISK_REPORT_ZBX_DATA']), json.dumps(zbx_data, indent=4))

                s = helper.read_file(os.path.join(cfg_py['LOG_DIR'], cfg_py['LOG_PROFILING']))
                s = f"{s}\n" if len(s) > 0 else ''
                s1 = f"seconds {(time.time() - start_time): >8.1f}    PAGE_SIZE {cfg_py['PAGE_SIZE']: >6}"                      f"      ORGs {len(orgs): >4}       vDCs {len(vdcs): >4}"                      f"      VMs {len(vms_basic): >6}      disks {len(disks): >6}"                      f"       misconfigurations {len(misconfigurations): >4}      delay {cfg_py['AIO_DELAY']: >6.3f}"                      f"    {cfg_py['LOG_LEVEL']:<8}    "
                s = f"{s}{s1}"

                helper.write_file(os.path.join(cfg_py['LOG_DIR'], cfg_py['LOG_PROFILING']), s)
                print(s1)

                if zbx_discover_result and zbx_data_result:
                    print("успешно завершено")
                else:
                    print("файлы для заббикс не записаны")

                return 0

Спасибо за внимание!


Что ещё интересного есть в блоге Cloud4Y

Частые ошибки в настройках Nginx, из-за которых веб-сервер становится уязвимым

Пароль как крестраж: ещё один способ защитить свои учётные данные

В США наблюдается рост спроса на VPN

Подготовка шаблона vApp тестовой среды VMware vCenter + ESXi

Почему ваш бизнес может быть разрушен

Подписывайтесь на наш Telegram-канал, чтобы не пропустить очередную статью. Пишем не чаще двух раз в неделю и только по делу.