From 4bb7c9244e2701b95365ab9b3f1bde00634040ca Mon Sep 17 00:00:00 2001 From: smolkik_adm Date: Thu, 3 Sep 2026 02:33:51 +0000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20API.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.py | 371 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 API.py diff --git a/API.py b/API.py new file mode 100644 index 0000000..b135326 --- /dev/null +++ b/API.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python +# coding: utf-8 +В таблице backups: size - это сколько реально занимают данные на носителе, а original_data_size - это сколько заимают реальные данные. +столбец deduplicated_size или равен нулю или original_data_size. +столбец backed_up_data_size или равен нулю или original_data_size, но значительно чаще чем deduplicated_size равен нулю. + +В таблице archives: size - это сколько реально занимают данные на носителе, а original_data_size и data_size - это сколько заимают реальные данные (все, что прошли через бэкап! даже те что уже проэкспаирились и удалились!!!). При этом непонятно зачем две разных колонки с одинаковыми значениями. +столбец logical_size или равен нулю или столбцу size. +столбец compressed_data_size или равен нулю или почти (чуть меньше!!!) равен столбцу size. +в таблице могут быть несколько строк с одинаковыми ['id' ,'name'], при этом разными будут ваульты. +# In[1]: + + +import pandas as pd +import numpy as np +import requests +from getpass import getpass +import os +from urllib3.exceptions import InsecureRequestWarning +from urllib.parse import quote +import datetime as dt + +import itables +itables.init_notebook_mode(connected=False) + +requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) +os.environ['no_proxy']='*' +cb_srv = 'nameserver' + +TB = (1024**4) # Для первода в терабайты +GB = (1024**3) # Для первода в гигабайты + + +# In[2]: + + +# Функция вытаскивает данные из эндпоинтов через пагинацию. +def get_paginated_data(uri, headers, limit=100, verify_ssl=False, timeout=30, progress=False, args = None): + all_items = [] + params = {"limit": limit} + + if args: + params.update(args) + + while True: + try: + response = requests.get( + uri, headers=headers, params=params, verify=verify_ssl, timeout=timeout + ) + + if response.status_code != 200: + print( + f"\033[91mОшибка: {response.status_code} — {response.reason}\033[0m" + ) + break + + data = response.json() + items = data.get("items", []) + all_items.extend(items) + + after_cursor = data.get("cursors", {}).get("after") or data.get( + "paging", {} + ).get("cursors", {}).get("after") + + if not after_cursor: + break + + params['after'] = after_cursor + + except Exception as e: + print(f"\033[91mОшибка: {e}\033[0m") + break + + if progress: + line_to_print = f"\rПолучено: {len(all_items)} элементов из {uri} " + print(f"\r{line_to_print}") + + return pd.json_normalize(all_items) if all_items else pd.DataFrame() + +# Функция конвртации времени в колонках на основании шаблона '_at' +def convert_time_many_cols(df): + # Список колонок для конвертации + iso_cols = [ + col + for col in df.columns if isinstance(col,str) and col.endswith("_at") + ] + + for col in iso_cols: + df[col] = ( + pd.to_datetime(df[col], format="ISO8601", errors='coerce',utc=True) + .dt.tz_convert("Europe/Moscow") + .dt.tz_localize(None) + ) + return df + + +# In[4]: + + +# Запрос пользователя/пароля +username = input("Введите имя пользователя: ") +password = getpass("Введите пароль: ") + +if not username or not password: + print("\033[91mНе введён User или Password\033[0m") +else: + auth_payload = { + 'username': username, + 'password': password, + 'grant_type': 'password' + } + +## Основная программа +# In[5]: + + +# Аутентификация +auth_url = f"https://{cb_srv}:9877/idp/token" +try: + auth_response = requests.post( + auth_url, + proxies={'http': None, 'https': None}, + data=auth_payload, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + verify=False + ) + auth_response.raise_for_status() + auth_data = auth_response.json() + # Проверка наличия токена + if 'token_type' not in auth_data or 'access_token' not in auth_data: + print("\033[91mОшибка: в ответе отсутствует токен\033[0m") + exit() +except requests.exceptions.RequestException as e: + print(f"\033[91mОшибка аутентификации: {e}\033[0m") + +headers = { + "Content-Type": "application/json", + "Authorization": f"{auth_data['token_type']} {auth_data['access_token']}" + } + + +# In[ ]: + + +# Получение списка бэкапов +df_backups = convert_time_many_cols( + get_paginated_data( + uri=f"https://{cb_srv}:9877/api/v1/backups", + headers=headers, + args={ + "include_screenshot_validation_status": "true", + # "embed": "archive", - 2026-06-10 убрал, т.к. мне не всё нужно из таблицы archives + #"include_actions": "true", -непонятно что даёт + }, + ) +) + + +# In[ ]: + + +# Получение списка архивов +df_archives = convert_time_many_cols( + get_paginated_data(uri=f"https://{cb_srv}:9877/api/v1/archives",headers=headers) +) +dict_archives = df_archives.set_index('id')['name'].to_dict() # id в списке архивов может встречаться несколько раз (при смене ваульта у задания, наверное и при дубликациина ленты). +dict_archives_plan = df_archives.set_index('id')['backup_plan_name'].to_dict() # id в списке архивов может встречаться несколько раз (при смене ваульта у задания, наверное и при дубликациина ленты). + + +# In[ ]: + + +# Получение списка ваультов (старое API) +df_vaults = convert_time_many_cols( + get_paginated_data(uri=f"https://{cb_srv}:9877/api/vault_manager/v1/vaults",headers=headers) +) +dict_vaults = df_vaults.set_index('id')['name'].to_dict() + + +# In[ ]: + + +# Получение списка политик +df_policies = convert_time_many_cols( + get_paginated_data( + uri=f"https://{cb_srv}:9877/api/v1/policies", + headers=headers, + args={"include_settings": "true", "include_applied_context": "true"}, + ) +) + + +# In[ ]: + + +# Получение информации о применении политик +df_policy_applications = convert_time_many_cols( + pd.json_normalize( + get_paginated_data( + uri=f"https://{cb_srv}:9877/api/v1/policy_applications", + headers=headers, + args={ + "include_validation_issues": "true", + "include_deployment_errors": "true", + "include_running_blockers": "true", +# "direct_only": "true", # Этот параметр уменьшает количество итоговых строк до 46. вместо примерно 1500! + }, + )[0] + ) +) + +## непоянтно что делают - не увидел разницы от применения: +# include_validation_issues=true +# include_deployment_errors=true +# include_running_blockers=true +# direct_only=true + + +# In[ ]: + + +# Получение списка устройств +df_resources = convert_time_many_cols( + get_paginated_data( + uri=f"https://{cb_srv}:9877/api/v1/resources", + headers=headers, + #args={"include_attributes": "true"}, + ) +) + +## Создаём словарь где значениями становится колонка name: +dict_resources = df_resources.set_index('id')['user_defined_name'].to_dict() +## приводим ключи в словаре в верхний регистр, т.к. в df_backups['resource_ids'] почему-то именно так: +dict_resources = {k.upper(): v for k,v in dict_resources.items()} + +## Создаём словарь где значениями становитя комбинация из "user_defined_name['name']" если user_defined_name не равно name, в противном случае user_defined_name: +dict_resources_comb = df_resources.set_index('id').apply(lambda r: r['user_defined_name'] if r['user_defined_name'] == r['name'] else f"{r['user_defined_name']} [{r['name']}]", axis=1).to_dict() +## приводим ключи в словаре в верхний регистр, т.к. в df_backups['resource_ids'] почему-то именно так: +dict_resources_comb = {k.upper(): v for k,v in dict_resources_comb.items()} + + +# In[ ]: + + +# заменяем GUID-ы в колонке 'resource_ids' на соответствующие значения из dict_resource. 'resource_ids' - это всегда list! +## Разворачиваем списки в отдельные строки, заменяем через map, и собираем обратно с \n +df_backups['resource_ids'] = ( + df_backups['resource_ids'] + .explode() # Превращает ['GUID1', 'GUID2'] в две строки: 'GUID1' и 'GUID2' + .pipe(lambda s: s.map(dict_resources_comb).fillna(s)) # если map вернёт NaN, то используется оригинальный GUID + .groupby(level=0) # Группируем обратно по исходному индексу + .apply(lambda x: '\n'.join(x)) # Склеиваем через перенос строки +) + +# Добавляем поле с именем плана защиты. назовём IS, так как потом из него будет сделан номер IS. Важно - это делается до замены GUID-ов в archive_id! +df_backups['IS'] = df_backups['archive_id'].map(dict_archives_plan) +# Заменяем GUID-ы на значения +df_backups['archive_id'] = df_backups['archive_id'].map(dict_archives).fillna(df_backups['archive_id']) +df_backups['vault_id'] = df_backups['vault_id'].map(dict_vaults).fillna(df_backups['vault_id']) + +# Добавляем столбец с типом устройства хранения - disk или tape. +set_libs_vault_ids = set(df_backups[df_backups['tapes'].notna()]['vault_id'].to_list()) +df_archives['vault_type'] = df_archives['vault.name'].apply(lambda x: 'tape' if x in set_libs_vault_ids else 'disk') + +# удаляем ненужные столбцы +df_backups = df_backups.drop(columns=['legacy_id','tenant_id']) +df_archives = df_archives.drop(columns=['vault_id','tenant_id','agent_id','backup_plan_id','vault.id','resource_id']) + +# создаём отдельные списки по типу хранения диски или ленты. +df_backups_disks = df_backups[df_backups['tapes'].isna()] +df_backups_libs = df_backups[df_backups['tapes'].notna()] + +# создаём отдельные списки по типу хранения диски или ленты в разрезе IS. +df_backups_IS = df_backups[(df_backups['IS'].notna())&(df_backups['IS'].str.startswith('IS'))].reset_index(drop=True) +df_backups_IS['IS'] = df_backups_IS['IS'].str.split('_').str.get(0) +df_backups_IS_disks = df_backups_IS[df_backups_IS['tapes'].isna()] +df_backups_IS_libs = df_backups_IS[df_backups_IS['tapes'].notna()] + +# создаём отдельные списки по типу хранения диски или ленты для списка archives. +df_archives_disks = df_archives[df_archives['vault_type']=='disk'] +df_archives_libs = df_archives[df_archives['vault_type']=='tape'] +# тоже, но в разрезе IS. Берём только те архивы, у которых в имени зашифрована IS и добавляем столбец с IS. +df_archives_IS = df_archives[(df_archives['backup_plan_name'].notna())&(df_archives['backup_plan_name'].str.startswith('IS'))].reset_index(drop=True) +df_archives_IS['IS'] = df_archives_IS['backup_plan_name'].str.split('_').str.get(0) +df_archives_IS_disks = df_archives_IS[df_archives_IS['vault_type']=='disk'].reset_index(drop=True) +df_archives_IS_libs = df_archives_IS[df_archives_IS['vault_type']=='lib'].reset_index(drop=True) + + +# In[ ]: + + +print(f'\033[32mБэкапов:\033[0m {len(df_backups)}') +print(f'\033[32mАрхивов:\033[0m {len(df_archives)}') +print(f'\033[32mВаультов:\033[0m {len(df_vaults)}') +print(f'\033[32mПолитик:\033[0m {len(df_policies)}') +print(f'\033[32mПрименений политик\033[0m: {len(df_policy_applications)}') + + +# In[ ]: + + +# Получение списка задач +df_tasks = convert_time_many_cols( + get_paginated_data(uri=f"https://{cb_srv}:9877/api/v1/tasks",headers=headers) +) + + +# In[ ]: + + +# Статистика задач +print(f"\033[32mВсего задач: {len(df_tasks)}") +print(f"Самая старая задача: {df_tasks['started_at'].min()}") +print(f"Самая новая задача: {df_tasks['started_at'].max()}") +nl='\n' +print(f"Перечень колонок у которых нет никаких значений:\033[0m \n{nl.join(df_tasks.columns[df_tasks.isna().all()].tolist())}") + + +# In[ ]: + + +df_archives_IS_disks.groupby(['IS']).agg(backEndOnDisk_TB=('size',lambda x: x.sum()/TB)) + + +# In[ ]: + + +df_archives_disks.groupby('resource_type').agg(backEndOnDisk_TB=('size',lambda x: x.sum()/TB),dataIn_TB=('data_size',lambda x: x.sum()/TB)) + + +# In[ ]: + + +df_archives_disks[['size','data_size']].sum().rename('TB')/TB + + +# In[ ]: + + +df_archives_disks['size'].sum()/TB + + +# In[ ]: + + +df_backups_IS_disks.query("type=='FULL'").groupby('archive_id').agg(backEndOnDisk_once_TB=('size',lambda x: x.max()/TB),dataFront_TB=('original_data_size',lambda x: x.max()/TB)).sum().rename('TB') + + +# In[ ]: + + +df_backups_disks.query("type=='FULL'").groupby('archive_id').agg(backEndOnDisk_once_TB=('size',lambda x: x.max()/TB),dataFront_TB=('original_data_size',lambda x: x.max()/TB)).sum().rename('TB') + + +# In[ ]: + + +## Вывод в Excel +xlsWriter = pd.ExcelWriter(f"{dt.datetime.now().strftime('%Y-%m-%d[%H-%M]')}_{cb_srv}.xlsx", engine='openpyxl', mode='w') +df_archives.to_excel(xlsWriter, sheet_name = "archives") +df_backups.to_excel(xlsWriter, sheet_name = "backups") +df_resources.to_excel(xlsWriter, sheet_name = "resources") +for worksheet in xlsWriter.sheets.values(): + for column in worksheet.columns: + length = max(len(str(cell.value)) for cell in column) + length = length if length <= 50 else 50 + worksheet.column_dimensions[column[0].column_letter].width = length+2 + worksheet.auto_filter.ref = worksheet.dimensions + worksheet.freeze_panes = 'A2' +xlsWriter.close() +