#!/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()