Files
CyberbackupAPI/API.py
2026-09-03 02:33:51 +00:00

372 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()