Добавить API.py
This commit is contained in:
371
API.py
Normal file
371
API.py
Normal file
@@ -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()
|
||||||
|
|
||||||
Reference in New Issue
Block a user