diff --git a/.env b/.env index 1a6a97f..e06aab6 100644 --- a/.env +++ b/.env @@ -1,8 +1,8 @@ -# vCenter配置(支持多个) -VCENTER_HOSTS=192.168.40.134 +# 多个vCenter地址(逗号分隔) +VCENTER_HOSTS=192.168.40.134,vc2.example.com,vc3.example.com +# 所有vCenter共用的账号密码 VCENTER_USER=administrator@lan.com VCENTER_PASSWORD=Root@2025 -# 快照保留天数 SNAPSHOT_RETENTION_DAYS=15 # 每台vCenter同时删除的快照数量限制 MAX_DELETE_CONCURRENT=4 diff --git a/README.md b/README.md index fd4bfbe..4b1a7c5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,69 @@ -# 以下需求需要每个月执行一次,使用ansible实现,还是使用python代码实行好? --[x] 获取所有vms --[ ] 获取所有snapshots --[ ] 筛选出15天(半个月)前的snapshots --[ ] 以上内容以Excel表格的形式导出 --[ ] 最后删除15天前的snapshot,并同时记录删除的snapshot日志信息 --[ ] 需要控制每台vCenter不可以同时删除超过4个快照 \ No newline at end of file +# RemoveWeeklySnapshot + +> 以下需求需要每周执行一次 + +- [x] 连接vCenter/Esxi/Hyper-V +- [x] 获取所有 vms +- [x] 获取所有 snapshots +- [x] 筛选出15天(半个月)前的snapshots +- [x] 以上内容以Excel表格的形式导出,超出15天的快照蓝底标识 +- [ ] 最后删除 15 天前的 snapshot,并同时记录删除的 snapshot 日志信息 +- [ ] 需要控制每台 vCenter 不可以同时删除超过4个快照 +- [ ] 增加排除不能删除的快照 + + + + + +| 你想获取 | 代码 | 示例输出 | +| ------------ | ----------------------------------- | ---------------------------------------------- | +| **名称** | `vm.name` | `"WebServer-01"` | +| **MOID** | `vm._moId` | `"vm-12"` | +| **电源状态** | `vm.runtime.powerState` | `poweredOn` / `poweredOff` | +| **开机时间** | `vm.runtime.bootTime` | `datetime` 对象 | +| **CPU数** | `vm.config.hardware.numCPU` | `4` | +| **内存(MB)** | `vm.config.hardware.memoryMB` | `8192` | +| **操作系统** | `vm.config.guestFullName` | `"CentOS 7 (64-bit)"` | +| **IP地址** | `vm.guest.ipAddress` | `"192.168.1.100"` | +| **主机名** | `vm.guest.hostName` | `"webserver01.local"` | +| **存储路径** | `vm.config.files.vmPathName` | `"[Datastore1] WebServer-01/WebServer-01.vmx"` | +| **快照数量** | `len(vm.snapshot.rootSnapshotList)` | `3` | + +>vm +>├── 基础标识 +>│ ├── name VM名称 +>│ └── _moId 内部ID (vm-12) +>│ +>├── runtime 【运行状态】 +>│ ├── powerState poweredOn/Off/Suspended +>│ ├── bootTime 开机时间 +>│ └── host 所在物理机 +>│ +>├── config 【硬件配置】 +>│ ├── hardware CPU/内存/硬盘 +>│ ├── guestFullName 操作系统 +>│ └── files VMX文件路径 +>│ +>├── guest 【客户机内部信息】 +>│ ├── hostName 主机名 +>│ ├── ipAddress IP地址 +>│ └── toolsStatus VMware Tools状态 +>│ +>├── snapshot 【快照】 +>│ └── rootSnapshotList 快照树 +>│ +>├── storage 【存储】 +>│ └── perDatastoreUsage 各数据存储用量 +>│ +>├── network 【网络】 +>│ └── [Network] 连接的端口组 +>│ +>└── summary 【快速汇总】 +>├── overallStatus 整体健康状态 +>└── quickStats 实时性能数据 + + + +[{'name': 'snap-02', 'description': 'test-Ansible snapshot', 'createTime': '2026-02-17 08:57:39', 'state': 'poweredOff', 'id': 2, 'moId': 'snapshot-27', 'sizeMB': None, 'quiesced': False, 'children': []}] +[{'name': 'snap-01', 'description': 'Ansible snapshot', 'createTime': '2026-02-17 03:26:08', 'state': 'poweredOff', 'id': 1, 'moId': 'snapshot-26', 'sizeMB': None, 'quiesced': False, 'children': [{'name': 'snap-02', 'description': 'test-Ansible snapshot', 'createTime': '2026-02-17 08:57:39', 'state': 'poweredOff', 'id': 2, 'moId': 'snapshot-27', 'sizeMB': None, 'quiesced': False, 'children': []}]}] +获取到 2 台VM diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..8f0603a --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,35 @@ +# 管理节点配置(包含vCenter和ESXi) +management_nodes: + # vCenter节点 + - type: vcenter # 标记类型为vcenter + name: vc1 # 节点名称(用于日志) + host: 192.168.40.134 # 地址 + user: administrator@lan.com + password: Root@2025 + max_delete_concurrent: 4 # 该节点最大并发删除数 + + # ESXi节点(未接入 vCenter 的 Esxi 主机) + - type: esxi # 标记类型为esxi + name: esxi1 + host: 192.168.40.133 + user: root # ESXi默认用root + password: Root@2025 + max_delete_concurrent: 2 # ESXi性能较弱,并发数可设小些 + + # 另一个ESXi节点 + - type: esxi + name: esxi2 + host: esxi2.example.com + user: root + password: esxi2_password + max_delete_concurrent: 2 + +# 全局策略配置 +global: + snapshot_retention_days: 15 + excel_output_path: ./vm_snapshots_report.xlsx + # excel_output_path: /tmp/vm_snapshots_report.xlsx + # log_file_path: /var/log/vm_snapshot_cleanup.log + log_file_path: ./vm_snapshot_cleanup.log + # ESXi连接特殊配置(禁用SSL验证,ESXi默认自签证书) + disable_ssl_verify: true \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index d9d6935..f5682aa 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,33 +1,88 @@ +import yaml import os from datetime import datetime, timedelta -from dotenv import load_dotenv +# from utils.logger import logger -# 加载.env文件 -load_dotenv() +# 配置文件路径 +CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config.yaml') -# ========== 基础配置 ========== -# vCenter配置 -VCENTER_HOSTS = os.getenv('VCENTER_HOSTS', '').split(',') -VCENTER_USER = os.getenv('VCENTER_USER', '') -VCENTER_PASSWORD = os.getenv('VCENTER_PASSWORD', '') -# 快照策略配置 -SNAPSHOT_RETENTION_DAYS = int(os.getenv('SNAPSHOT_RETENTION_DAYS', 15)) -MAX_DELETE_CONCURRENT = int(os.getenv('MAX_DELETE_CONCURRENT', 4)) +def load_config(): + """加载YAML配置,区分vCenter和ESXi""" + try: + with open(CONFIG_PATH, 'r', encoding='utf-8') as f: + raw_config = yaml.safe_load(f) -# 输出路径配置 -EXCEL_OUTPUT_PATH = os.getenv('EXCEL_OUTPUT_PATH', '/tmp/vm_snapshots_report.xlsx') -LOG_FILE_PATH = os.getenv('LOG_FILE_PATH', '/var/log/vm_snapshot_cleanup.log') + # 全局配置 + global_config = raw_config.get('global', {}) + config = { + # vCenter/ESXi节点列表 + "MANAGEMENT_NODES": raw_config.get('management_nodes', []), + # vCenter/ESXi节点列表 + "SNAPSHOT_RETENTION_DAYS": int(global_config.get('snapshot_retention_days', 15)), + "EXCEL_OUTPUT_PATH": global_config.get('excel_output_path', '/tmp/vm_snapshots_report.xlsx'), + "LOG_FILE_PATH": global_config.get('log_file_path', '/var/log/vm_snapshot_cleanup.log'), + "DISABLE_SSL_VERIFY": global_config.get('disable_ssl_verify', True), + # 算出的过期时间点 + "EXPIRE_DATE": datetime.now() - timedelta(days=int(global_config.get('snapshot_retention_days', 15))) + } -# 计算快照过期时间(全局变量) -EXPIRE_DATE = datetime.now() - timedelta(days=SNAPSHOT_RETENTION_DAYS) + # 验证配置 + if not config["MANAGEMENT_NODES"]: + raise ValueError("未配置任何管理节点(vCenter/ESXi)") -# 验证必要配置 + # 检查每个节点的必填字段 + required_fields = ['type', 'name', 'host', 'user', 'password', 'max_delete_concurrent'] + for node in config["MANAGEMENT_NODES"]: + missing = [f for f in required_fields if f not in node] + if missing: + raise ValueError(f"节点 {node.get('name', '未知')} 缺少配置字段: {missing}") + # 验证类型合法性 + if node['type'] not in ['vcenter', 'esxi']: + raise ValueError(f"节点 {node['name']} 类型错误(仅支持vcenter/esxi)") + + #logger.info(f"✅ 成功加载配置,共 {len(config['MANAGEMENT_NODES'])} 个管理节点") + return config + + except Exception as e: + #logger.error(f"❌ 加载配置失败: {str(e)}") + raise + + +# 加载配置并导出全局变量 +config = load_config() # 模块导入时立即执行 +MANAGEMENT_NODES = config["MANAGEMENT_NODES"] +SNAPSHOT_RETENTION_DAYS = config["SNAPSHOT_RETENTION_DAYS"] +EXCEL_OUTPUT_PATH = config["EXCEL_OUTPUT_PATH"] +LOG_FILE_PATH = config["LOG_FILE_PATH"] +DISABLE_SSL_VERIFY = config["DISABLE_SSL_VERIFY"] +EXPIRE_DATE = config["EXPIRE_DATE"] + + +# 验证配置函数 def validate_config(): - """验证配置是否完整""" - required = [ - VCENTER_HOSTS, VCENTER_USER, VCENTER_PASSWORD, - SNAPSHOT_RETENTION_DAYS, MAX_DELETE_CONCURRENT - ] - if not all(required) or '' in VCENTER_HOSTS: - raise ValueError("配置不完整,请检查.env文件中的vCenter信息和策略配置") \ No newline at end of file + pass # 加载时已验证,此处留空 + + +if __name__ == "__main__": + # 打印全局配置 + print("\n【全局配置】") + print(f" 快照保留天数: {config['SNAPSHOT_RETENTION_DAYS']}") + print(f" 过期日期: {config['EXPIRE_DATE']}") + print(f" Excel输出路径: {config['EXCEL_OUTPUT_PATH']}") + print(f" 日志文件路径: {config['LOG_FILE_PATH']}") + print(f" 禁用SSL验证: {config['DISABLE_SSL_VERIFY']}") + + # 打印管理节点 + nodes = config['MANAGEMENT_NODES'] + print(f"\n【管理节点】共 {len(nodes)} 个") + + for i, node in enumerate(nodes, 1): + print(f"\n 节点[{i}]:") + print(f" 类型: {node.get('type')}") + print(f" 名称: {node.get('name')}") + print(f" 地址: {node.get('host')}") + print(f" 用户: {node.get('user')}") + print(f" 密码: {'*' * len(node.get('password', ''))}") + # print(f" 密码: {node.get('password', '')}") # 直接打印出密码 + print(f" 最大并发删除: {node.get('max_delete_concurrent')}") \ No newline at end of file diff --git a/core.py b/core.py new file mode 100644 index 0000000..675a74a --- /dev/null +++ b/core.py @@ -0,0 +1,2 @@ +def vcenter_connector(): + return None \ No newline at end of file diff --git a/core/get_vms.py b/core/get_vms.py new file mode 100644 index 0000000..b91ffb8 --- /dev/null +++ b/core/get_vms.py @@ -0,0 +1,237 @@ +from pyVmomi import vim +from pyVim.connect import SmartConnect, Disconnect +from config.settings import MANAGEMENT_NODES +from datetime import timedelta +import pandas as pd +import openpyxl +from openpyxl.styles import Border, Side, Font, PatternFill +from datetime import datetime, timedelta + + +"""计算虚拟机的总磁盘大小(仅是虚拟磁盘的分配空间)""" +def get_virtual_disk_size(vm): + total_size = 0 + for device in vm.config.hardware.device: + if isinstance(device, vim.vm.device.VirtualDisk): + # 获取每个虚拟磁盘的容量 + total_size += device.capacityInKB / (1024 * 1024) # 转换为GB + return total_size + + +"""获取所有虚拟机""" +def get_all_vms(): + # 取第一个节点 + node = MANAGEMENT_NODES[0] + + # 连接vCenter + si = SmartConnect( + host=node['host'], + user=node['user'], + pwd=node['password'], + disableSslCertValidation=True + ) + + # 获取所有VM + content = si.RetrieveContent() + vm_view = content.viewManager.CreateContainerView( + content.rootFolder, [vim.VirtualMachine], True + ) + + vm_list = [] + for vm in vm_view.view: + # 初始化VM信息字典 + vm_info = { + 'name': vm.name, + 'moId': vm._moId, + 'powerState': str(vm.runtime.powerState), + 'system': vm.config.guestFullName, + 'ipAddress': vm.guest.ipAddress, + 'hostName': vm.guest.hostName, + 'vmPath': vm.config.files.vmPathName, + 'Host': vm.runtime.host.name, # 拿到 Host 主机名 + 'snapshots': [], # 添加快照信息 + 'diskSpaceGB': get_virtual_disk_size(vm) # 添加虚拟机占用的磁盘空间 + } + + # 获取快照信息 + if vm.snapshot is not None: + current_snapshot = vm.snapshot.currentSnapshot + root_snapshots = vm.snapshot.rootSnapshotList + + # 处理根快照列表 + for snapshot in root_snapshots: + snapshot_info = get_snapshot_info(snapshot) + vm_info['snapshots'].extend(snapshot_info) + + # 添加当前快照ID(如果有) + if current_snapshot: + vm_info['currentSnapshotId'] = current_snapshot._moId + else: + vm_info['snapshots'] = None + vm_info['currentSnapshotId'] = None + + vm_list.append(vm_info) + + vm_view.Destroy() + Disconnect(si) + + print(f"获取到 {len(vm_list)} 台VM") + return vm_list + + +"""递归获取快照信息""" +def get_snapshot_info(snapshot): + + snapshot_list = [] + # 当前快照信息 + snapshot_info = { + 'name': snapshot.name, + 'description': snapshot.description, + 'createTime': (snapshot.createTime + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'), + 'state': str(snapshot.state), + 'id': snapshot.id, + 'moId': snapshot.snapshot._moId, # 快照的Managed Object ID + 'sizeMB': snapshot.diskSizeMB if hasattr(snapshot, 'diskSizeMB') else None, # 快照大小 + 'quiesced': snapshot.quiesced if hasattr(snapshot, 'quiesced') else None, # 是否静默快照 + 'children': [] # 子快照 + } + + # 递归处理子快照 + if snapshot.childSnapshotList: + for child in snapshot.childSnapshotList: + snapshot_info['children'].extend(get_snapshot_info(child)) + + snapshot_list.append(snapshot_info) + return snapshot_list + + +def print_snapshot_tree(snapshot_info, level=0): + """递归打印快照树(辅助函数)""" + indent = " " * level + print(f"{indent}├─ {snapshot_info['name']}") + print(f"{indent}│ ├─ 创建时间: {snapshot_info['createTime']}") + print(f"{indent}│ ├─ 描述: {snapshot_info['description']}") + print(f"{indent}│ ├─ 状态: {snapshot_info['state']}") + if snapshot_info['sizeMB']: + print(f"{indent}│ ├─ 大小: {snapshot_info['sizeMB']} MB") + + for child in snapshot_info['children']: + print_snapshot_tree(child, level + 1) + + +"""表格样式""" +def style_sheet(sheet, is_old_data=None): + """设置工作表样式:列宽、边框、加粗标题、冻结首行和设置背景颜色""" + # 定义边框样式 + thin = Side(border_style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + + # 设置列宽和边框 + for column in sheet.columns: + max_length = 0 + column_letter = column[0].column_letter + + for cell in column: + try: + if len(str(cell.value)) > max_length: + max_length = len(str(cell.value)) + cell.border = border # 为每个单元格添加边框 + except Exception as e: + pass + + adjusted_width = (max_length + 2) + sheet.column_dimensions[column_letter].width = adjusted_width + + # 加粗标题并冻结首行 + for cell in sheet[1]: # 加粗标题 + cell.font = Font(bold=True) + sheet.freeze_panes = 'A2' # 冻结首行 + + # 设置背景颜色 + if is_old_data is not None: + for row in range(2, len(is_old_data) + 2): + if is_old_data[row - 2]: + for col in range(1, sheet.max_column + 1): # 使用 sheet.max_column + cell = sheet.cell(row=row, column=col) + cell.fill = PatternFill(start_color='ADD8E6', end_color='ADD8E6', fill_type='solid') + + +"""将虚拟机和快照信息写入Excel文件,并标记创建时间在15天前的快照""" +def create_excel_report(vms): + vm_data = [] + snapshot_data = [] + + def add_snapshots_to_report(snapshot, vm_name): + """递归将快照信息加入报告""" + create_time = datetime.strptime(snapshot['createTime'], '%Y-%m-%d %H:%M:%S') + is_old = create_time < (datetime.now() - timedelta(days=1)) + + snapshot_data.append({ + 'VM Name': vm_name, + 'Snapshot Name': snapshot['name'], + 'Description': snapshot['description'], + 'Create Time': snapshot['createTime'], + 'State': snapshot['state'], + 'ID': snapshot['id'], + 'MO ID': snapshot['moId'], + 'Size (MB)': snapshot['sizeMB'], + 'Quiesced': snapshot['quiesced'], + 'is_old': is_old + }) + + for child in snapshot['children']: + add_snapshots_to_report(child, vm_name) + + for vm in vms: + vm_data.append({ + 'VM Name': vm['name'], + 'MO ID': vm['moId'], + 'Power State': vm['powerState'], + 'System': vm['system'], + 'IP Address': vm['ipAddress'], + 'Host Name': vm['hostName'], + 'VM Path': vm['vmPath'], + 'Host': vm['Host'], + 'Current Snapshot ID': vm.get('currentSnapshotId', None), + 'DiskSpace/GB': vm['diskSpaceGB'] + }) + + if vm['snapshots']: + for snapshot in vm['snapshots']: + add_snapshots_to_report(snapshot, vm['name']) + + vm_df = pd.DataFrame(vm_data) + snapshot_df = pd.DataFrame(snapshot_data) + + with pd.ExcelWriter('vm_and_snapshots_report.xlsx', engine='openpyxl') as writer: + vm_df.to_excel(writer, sheet_name='VMs', index=False) + snapshot_df.to_excel(writer, sheet_name='Snapshots', index=False) + + # 获取写入的工作表 + workbook = writer.book + vm_sheet = workbook['VMs'] + snapshot_sheet = workbook['Snapshots'] + + # 调用样式设置函数,传入 is_old_data + style_sheet(vm_sheet) # 设置 VMs 工作表样式 + style_sheet(snapshot_sheet, snapshot_df['is_old'].tolist()) # 设置 Snapshots 工作表样式并传入旧数据标识列表 + + print("报告已生成: vm_and_snapshots_report.xlsx") + + + +if __name__ == '__main__': + vms = get_all_vms() + + # # 打印示例:显示每个VM的快照信息 + # for vm in vms: + # print(f"\nVM: {vm['name']}") + # if vm['snapshots']: + # print(f"快照数量: {len(vm['snapshots'])}") + # for snapshot in vm['snapshots']: + # print_snapshot_tree(snapshot) + # else: + # print("无快照") + + # 生成Excel报告 + create_excel_report(vms) diff --git a/core/snapshot_collector.py b/core/snapshot_collector.py new file mode 100644 index 0000000..9448151 --- /dev/null +++ b/core/snapshot_collector.py @@ -0,0 +1,105 @@ +from pyVmomi import vim +from config.settings import MANAGEMENT_NODES +from core.vcenter_connector import VCenterManager # 使用管理器类 +from utils.logger import logger +from typing import List, Dict + + +def get_all_vms_from_node(si, node_name: str) -> List[Dict]: + """ + 从单个vCenter/ESXi获取所有虚拟机 + :param si: ServiceInstance + :param node_name: 节点名称(用于标识来源) + :return: VM列表 + """ + try: + content = si.RetrieveContent() + vm_view = content.viewManager.CreateContainerView( + content.rootFolder, [vim.VirtualMachine], True + ) + vms = vm_view.view + vm_view.Destroy() + + vm_list = [] + for vm in vms: + # 安全获取快照数量 + num_snapshots = 0 + if vm.snapshot and vm.snapshot.rootSnapshotList: + num_snapshots = len(vm.snapshot.rootSnapshotList) + + vm_info = { + 'vcenter_node': node_name, # 来自哪个vCenter/ESXi + 'vcenter_host': si._stub.host, # vCenter主机地址 + 'vm_name': vm.name, + 'vm_moid': vm._moId, + 'power_state': str(vm.runtime.powerState), # 转字符串 + 'num_snapshots': num_snapshots, + 'guest_os': vm.config.guestFullName if vm.config else 'Unknown' + } + vm_list.append(vm_info) + + logger.info(f"📥 [{node_name}] 获取到 {len(vm_list)} 台VM") + return vm_list + + except Exception as e: + logger.error(f"❌ [{node_name}] 获取VM列表失败: {str(e)}") + return [] + + +def get_all_vms_from_all_nodes() -> List[Dict]: + """ + 从所有配置的vCenter/ESXi节点获取虚拟机 + :return: 合并后的VM列表 + """ + all_vms = [] + + # 使用VCenterManager连接所有节点 + with VCenterManager() as manager: + connections = manager.connect_all() + + if not connections: + logger.error("没有可用的vCenter/ESXi连接") + return [] + + # 遍历每个连接获取VM + for conn in connections: + vms = get_all_vms_from_node(conn.si, conn.name) + all_vms.extend(vms) # 合并列表 + + logger.info(f"📊 总计获取 {len(all_vms)} 台VM(来自 {len(connections)} 个节点)") + return all_vms + + +def print_vm_table(vm_list: List[Dict]): + """打印VM列表表格(调试用)""" + if not vm_list: + print("没有获取到VM数据") + return + + print(f"\n{'=' * 100}") + print(f"{'节点':<15} {'VM名称':<30} {'状态':<12} {'快照数':<8} {'操作系统':<20}") + print(f"{'-' * 100}") + + for vm in vm_list: + print(f"{vm['vcenter_node']:<15} {vm['vm_name']:<30} " + f"{vm['power_state']:<12} {vm['num_snapshots']:<8} " + f"{vm['guest_os'][:20]:<20}") + + print(f"{'=' * 100}") + print(f"总计: {len(vm_list)} 台VM") + + +# ==================== 主程序入口 ==================== + +if __name__ == '__main__': + # 获取所有VM + vm_list = get_all_vms_from_all_nodes() + + # 打印结果 + print_vm_table(vm_list) + + # 示例:筛选有快照的VM + vms_with_snapshots = [vm for vm in vm_list if vm['num_snapshots'] > 0] + print(f"\n有快照的VM: {len(vms_with_snapshots)} 台") + for vm in vms_with_snapshots: + print(f" - {vm['vm_name']}: {vm['num_snapshots']} 个快照") \ No newline at end of file diff --git a/core/vcenter_connector.py b/core/vcenter_connector.py new file mode 100644 index 0000000..8f0621b --- /dev/null +++ b/core/vcenter_connector.py @@ -0,0 +1,184 @@ +from pyVim.connect import SmartConnect, Disconnect +from pyVmomi import vim +from config.settings import MANAGEMENT_NODES +from utils.logger import logger +from dataclasses import dataclass +from typing import List, Optional, Dict + + +@dataclass +class Connection: + """包装连接对象,包含节点信息和ServiceInstance""" + name: str # 节点名称 + host: str # 主机地址 + node_type: str # vcenter 或 esxi + si: any # ServiceInstance + max_concurrent: int # 该节点的最大并发数 + + +class VCenterManager: + """管理多个vCenter/ESXi连接""" + + def __init__(self): + # 构造方法,创建对象时自动执行 + self.connections: List[Connection] = [] # 属性1:成功连接列表 + self.failed_nodes: List[Dict] = [] # 属性2:失败节点列表 + + def connect_all(self) -> List[Connection]: + """ + 连接所有配置的管理节点 + :return: 成功连接的列表 + """ + logger.info(f"开始连接 {len(MANAGEMENT_NODES)} 个管理节点...") + + for node in MANAGEMENT_NODES: + name = node['name'] + host = node['host'] + user = node['user'] + password = node['password'] + node_type = node['type'] + max_concurrent = node.get('max_delete_concurrent', 4) + + logger.info(f"正在连接节点: {name} ({host}) [{node_type}]") + + si = self._connect_single(host, user, password, node_type) + + if si: + conn = Connection( + name=name, + host=host, + node_type=node_type, + si=si, + max_concurrent=max_concurrent + ) + self.connections.append(conn) + logger.info(f"✅ {name} 连接成功") + else: + self.failed_nodes.append({ + 'name': name, + 'host': host, + 'type': node_type + }) + logger.error(f"❌ {name} 连接失败") + + logger.info(f"连接完成: 成功 {len(self.connections)}/{len(MANAGEMENT_NODES)}") + return self.connections + + def _connect_single(self, host: str, user: str, password: str, node_type: str): + """连接单个节点""" + try: + si = SmartConnect( + host=host, + user=user, + pwd=password, + disableSslCertValidation=True + ) + return si + except Exception as e: + logger.error(f"连接 {host} 失败: {str(e)}") + return None + + def get_connection(self, name: str = None) -> Optional[Connection]: + """ + 获取指定名称的连接,不指定则返回第一个 + """ + if not self.connections: + return None + + if name: + for conn in self.connections: + if conn.name == name: + return conn + return None + + return self.connections[0] + + def get_all_connections(self) -> List[Connection]: + """获取所有成功连接""" + return self.connections + + def get_vcenter_connections(self) -> List[Connection]: + """仅获取 vcenter 类型的连接""" + return [c for c in self.connections if c.node_type == 'vcenter'] + + def get_esxi_connections(self) -> List[Connection]: + """仅获取 esxi 类型的连接""" + return [c for c in self.connections if c.node_type == 'esxi'] + + def disconnect_all(self): + """关闭所有连接""" + for conn in self.connections: + try: + Disconnect(conn.si) + logger.info(f"已断开 {conn.name}") + except Exception as e: + logger.warning(f"断开 {conn.name} 时出错: {e}") + self.connections = [] + + def __enter__(self): + """上下文管理器支持""" + self.connect_all() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """确保退出时断开连接""" + self.disconnect_all() + + +# ==================== 使用示例 ==================== + +def main(): + # 方式1:手动管理连接 + manager = VCenterManager() + connections = manager.connect_all() + + # 遍历所有连接执行操作 + for conn in connections: + print(f"\n处理节点: {conn.name} ({conn.host})") + print(f"最大并发: {conn.max_concurrent}") + + # 获取内容视图 + content = conn.si.RetrieveContent() + + # 获取所有VM + from pyVmomi import vim + obj_view = content.viewManager.CreateContainerView( + content.rootFolder, [vim.VirtualMachine], True + ) + vms = obj_view.view + print(f"该节点有 {len(vms)} 台虚拟机") + obj_view.Destroy() + + # 断开所有连接 + manager.disconnect_all() + + +def main_with_context(): + # 方式2:使用上下文管理器(推荐,自动断开) + with VCenterManager() as manager: + # 获取所有vcenter连接 + vcenters = manager.get_vcenter_connections() + + for conn in vcenters: + print(f"处理vCenter: {conn.name}") + # 执行操作... + + # 退出时自动断开所有连接 + + +def main_single_operation(): + # 方式3:只连接特定节点 + manager = VCenterManager() + manager.connect_all() + + # 获取指定节点 + prod_vc = manager.get_connection('vcenter-prod') + if prod_vc: + print(f"连接到生产环境: {prod_vc.host}") + # 执行操作... + + manager.disconnect_all() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..1c0553f --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,34 @@ +import logging +from config.settings import LOG_FILE_PATH + + +def setup_logger(): + """配置日志系统,返回logger实例""" + # 创建logger + logger = logging.getLogger('vm_snapshot_cleanup') + logger.setLevel(logging.INFO) + + # 避免重复添加处理器 + if logger.handlers: + return logger + + # 定义日志格式 + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + + # 文件处理器(写入日志文件) + file_handler = logging.FileHandler(LOG_FILE_PATH, encoding='utf-8') + file_handler.setFormatter(formatter) + + # 控制台处理器(输出到终端) + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + + # 添加处理器 + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + return logger + + +# 全局logger实例 +logger = setup_logger() \ No newline at end of file diff --git a/vcenter_connector.py b/vcenter_connector.py deleted file mode 100644 index 1750a0c..0000000 --- a/vcenter_connector.py +++ /dev/null @@ -1,40 +0,0 @@ -from pyVim.connect import SmartConnect, Disconnect -from pyVmomi import vim -from config.settings import VCENTER_USER, VCENTER_PASSWORD -from utils.logger import logger - - -def connect_to_vcenter(vcenter_host): - """ - 连接到指定的vCenter服务器 - :param vcenter_host: vCenter主机名/IP - :return: 成功返回ServiceInstance,失败返回None - """ - try: - # 禁用SSL证书验证(生产环境建议启用证书验证) - si = SmartConnect( - host=vcenter_host, - user=VCENTER_USER, - pwd=VCENTER_PASSWORD, - disableSslCertValidation=True - ) - if not si: - logger.error(f"❌ 无法连接到vCenter: {vcenter_host}(无返回实例)") - return None - - logger.info(f"✅ 成功连接到vCenter: {vcenter_host}") - return si - - except Exception as e: - logger.error(f"❌ 连接vCenter {vcenter_host} 失败: {str(e)}") - return None - - -def disconnect_from_vcenter(si): - """ - 关闭vCenter连接 - :param si: ServiceInstance实例 - """ - if si: - Disconnect(si) - logger.debug("已关闭vCenter连接") \ No newline at end of file