diff --git a/.env b/.env deleted file mode 100644 index 0ab023a..0000000 --- a/.env +++ /dev/null @@ -1,14 +0,0 @@ -# 多个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 -# Excel导出路径 -# EXCEL_OUTPUT_PATH=/tmp/vm_snapshots_report.xlsx -EXCEL_OUTPUT_PATH=./vm_snapshots_report.xlsx -# 日志文件路径 -# LOG_FILE_PATH=/var/logs/vm_snapshot_cleanup.logs -LOG_FILE_PATH=./log/vm_snapshot_cleanup.log \ No newline at end of file diff --git a/README.md b/README.md index f355b0a..4c09dca 100644 --- a/README.md +++ b/README.md @@ -400,8 +400,8 @@ D:. │ README.md # 项目描述 │ ├─config # 项目程序配置文件 -│ │ config.yaml -│ │ settings.py +│ │ config.yaml # 配置文件 +│ │ settings.py # 配置加载和全局变量 │ ├─core # 核心程序 │ │ deleteSnapshots.py @@ -414,8 +414,8 @@ D:. │ 2026-02-20-old_snapshots.yaml │ 2026-02-20_20-36-45-VMsSnapShots_report.xlsx │ -├─utils # 日志输出格式设置 -│ │ logger.py +├─utils # 工具函数 +│ │ logger.py # 日志配置 ``` @@ -423,11 +423,27 @@ D:. ## 所用到的 Python 库 +``` powershell +PS D:\PycharmProjects\RemoveWeeklyShapshot> pip freeze > requirements.txt +openpyxl==3.2.0b1 +pandas==3.0.1 +pyvmomi==9.0.0.0 +PyYAML==6.0.3 + +``` + ``` shell -pip install pyVmomi ... +pip install -r requirements.txt ``` +## **定时执行**(Linux crontab): + +```bash +# 每月1号凌晨2点执行 +0 2 1 * * /usr/bin/python3 /var/RemoveWeeklyShapshot/main.py 2>&1 +``` + diff --git a/config/config.yaml b/config/config.yaml index 25a77ae..2fb7592 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,32 +1,32 @@ # 管理节点配置(包含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 # 该节点最大并发删除数 +# - 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性能较弱,并发数可设小些 + - type: esxi # 标记类型为esxi + name: esxi1 + host: 192.168.40.133 + user: root # ESXi默认用root + password: Root@2025 + max_delete_concurrent: 2 # ESXi性能较弱,并发数可设小些 -# - type: esxi -# name: esxi2 -# host: 192.168.40.135 -# user: root -# password: Root@2025 -# max_delete_concurrent: 2 + - type: esxi + name: esxi2 + host: 192.168.40.135 + user: root + password: Root@2025 + max_delete_concurrent: 2 # 全局策略配置 global: - snapshot_retention_days: 0 # 可选,使用默认值 15 天 -# excel_output_path: ./vm_snapshots_report.xlsx # 可选,使用默认值,如:/logs/2026-02-20_14-00-21-VMsSnapShots_report.xlsx -# 'excel_output_path',: ./vm_snapshots_report.xlsx # 可选,使用默认值,如:/logs/2026-02-20_14-00-21-VMsSnapShots_report.xlsx - # ESXi连接特殊配置(禁用SSL验证,ESXi默认自签证书) - disable_ssl_verify: true \ No newline at end of file + disable_ssl_verify: true + snapshot_retention_days: 15 # 可选,默认值 15 天 +# excel_output_path: ./vm_snapshots_report.xlsx # 可选,使用默认值,如:/logs/2026-02-20_14-00-21-VMsSnapShots_report.xlsx +# 'excel_output_path',: ./vm_snapshots_report.xlsx # 可选,使用默认值,如:/logs/2026-02-20_14-00-21-VMsSnapShots_report.xlsx +# ESXi连接特殊配置(禁用SSL验证,ESXi默认自签证书) diff --git a/config/settings.py b/config/settings.py index 25fd513..0f62c2f 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,8 +1,8 @@ -import yaml -import os +import yaml, os from datetime import datetime from utils.logger import logger + def load_config(): """加载 YAML 配置文件并解析其内容""" try: @@ -17,10 +17,9 @@ def load_config(): "SNAPSHOT_RETENTION_DAYS": int(global_config.get('snapshot_retention_days', 15)), # "EXCEL_OUTPUT_PATH": global_config.get('excel_output_path', f'/logs/{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}-VMsSnapShots_report.xlsx'), # "LOG_FILE_PATH": global_config.get('log_file_path', f'/logs/{datetime.now().strftime('%Y%m%d_%H%M%S')}-VMsSnapShots_cleanup.logs'), "EXCEL_OUTPUT_PATH": global_config.get('excel_output_path', f'/logs/{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}-VMsSnapShots_report.xlsx'), - "EXCEL_OUTPUT_PATH": global_config.get('excel_output_path', f'D:\\PycharmProjects\\RemoveWeeklyShapshot\\output\\{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}-VMsSnapShots_report.xlsx'), - "YAML_OUTPUT_PATH": global_config.get('yaml_output_path', f'D:\\PycharmProjects\\RemoveWeeklyShapshot\\output\\{datetime.now().strftime('%Y-%m-%d')}-old_snapshots.yaml'), + "EXCEL_OUTPUT_PATH": global_config.get('excel_output_path', f'D:\\PycharmProjects\\RemoveWeeklySnapshot\\output\\vm_snapshots_report-{datetime.now().strftime('%Y-%m-%d')}.xlsx'), + "YAML_OUTPUT_PATH": global_config.get('yaml_output_path', f'D:\\PycharmProjects\\RemoveWeeklySnapshot\\output\\old_snapshots-{datetime.now().strftime('%Y-%m-%d')}.yaml'), "DISABLE_SSL_VERIFY": global_config.get('disable_ssl_verify', True), - # 算出的过期时间点 } # 验证配置 @@ -54,10 +53,6 @@ YAML_OUTPUT_PATH = config["YAML_OUTPUT_PATH"] #LOG_FILE_PATH = config["LOG_FILE_PATH"] DISABLE_SSL_VERIFY = config["DISABLE_SSL_VERIFY"] -# 验证配置函数 -def validate_config(): - pass # 加载时已验证,此处留空 - if __name__ == "__main__": # 打印全局配置 diff --git a/core.py b/core.py deleted file mode 100644 index 675a74a..0000000 --- a/core.py +++ /dev/null @@ -1,2 +0,0 @@ -def vcenter_connector(): - return None \ No newline at end of file diff --git a/core/getVmsSnapshots.py b/core/data_exporter.py similarity index 51% rename from core/getVmsSnapshots.py rename to core/data_exporter.py index ce4f45b..306bd1d 100644 --- a/core/getVmsSnapshots.py +++ b/core/data_exporter.py @@ -1,101 +1,80 @@ -import pandas as pd import yaml -from pyVmomi import vim +import pandas as pd from datetime import datetime, timedelta -from pyVim.connect import SmartConnect, Disconnect from openpyxl.styles import Border, Side, Font, PatternFill from utils.logger import logger -from config.settings import MANAGEMENT_NODES, SNAPSHOT_RETENTION_DAYS, EXCEL_OUTPUT_PATH, YAML_OUTPUT_PATH +from config.settings import EXCEL_OUTPUT_PATH, YAML_OUTPUT_PATH, SNAPSHOT_RETENTION_DAYS +from core.get_vm_snapshots import get_all_vms + +# 输出数据到 Excel 文件 +def create_excel_report(vms): + vm_data, snapshot_data, old_snapshots = [], [], [] + for vm in vms: + vm_data.append({ + 'NodeHost': vm['NodeHost'], + 'VMName': vm['name'], + 'MOID': vm['moId'], + 'PowerState': vm['powerState'], + 'System': vm['system'], + 'IPAddress': vm['ipAddress'], + 'HostName': vm['hostName'], + 'CurrentSnapshotID': vm.get('currentSnapshotId'), + 'DiskSpace/GB': vm['diskSpaceGB'], + 'createDate': vm['createDate'], # 虚拟机的创建时间 + 'bootTime': vm['bootTime'], # 虚拟机上次启动的时间 + 'Host': vm['Host'], + 'VMPath': vm['vmPath'] + }) + + if vm['snapshots']: + for snapshot in vm['snapshots']: + collect_snapshot_data(snapshot, vm, snapshot_data, old_snapshots) + + vm_df = pd.DataFrame(vm_data) + snapshot_df = pd.DataFrame(snapshot_data) + logger.info(f"总共有 {len(snapshot_data)} 个快照") + with pd.ExcelWriter(EXCEL_OUTPUT_PATH, 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 + style_sheet(workbook['VMs']) + # style_sheet(workbook['Snapshots'], snapshot_df['is_old'].tolist()) + # 判断 DataFrame 是否有数据,没有数据则跳过,有数据则写入 excel 文件 + if 'is_old' in snapshot_df.columns and snapshot_df['is_old'].tolist(): + style_sheet(workbook['Snapshots'], snapshot_df['is_old'].tolist()) + + logger.debug(f"Excel 文件已生成: {EXCEL_OUTPUT_PATH}") + # 返回旧快照的数据,用于生成 Yaml 数据文件 + return old_snapshots -def get_all_vms(): - """ 主函数,负责流程控制""" - vm_list = [] +def collect_snapshot_data(snapshot, vm, snapshot_data, old_snapshots): + """ 递归快照数据用于 Excel,并收集旧快照 """ + create_time = datetime.strptime(snapshot['createTime'], '%Y-%m-%d %H:%M:%S') + is_old = create_time < (datetime.now() - timedelta(days=SNAPSHOT_RETENTION_DAYS)) - for node in MANAGEMENT_NODES: - try: - si = SmartConnect( - host=node['host'], - user=node['user'], - pwd=node['password'], - disableSslCertValidation=True - ) - - content = si.RetrieveContent() - vm_view = content.viewManager.CreateContainerView( - content.rootFolder, [vim.VirtualMachine], True - ) - - for vm in vm_view.view: - vm_info = build_vm_info(vm, node['host']) - vm_list.append(vm_info) - - except vim.fault.InvalidLogin as e: - logger.info(f"登录 {node['host']} 失败,请检查用户名和密码:{e.msg}") - except Exception as e: - logger.error(f"无法连接到 {node['host']}:{e}") - finally: - if 'si' in locals(): # 确保 si 是定义过的 - Disconnect(si) # 确保连接被断开 - - print(f"获取到 {len(vm_list)} 台VM") - return vm_list - - -def build_vm_info(vm, node_host): - """构造虚拟机信息字典""" - vm_info = { - 'NodeHost': node_host, - 'name': vm.name, - 'moId': vm._moId, - 'powerState': vm.runtime.powerState, - 'system': vm.config.guestFullName, - 'ipAddress': vm.guest.ipAddress, - 'hostName': vm.guest.hostName, - 'diskSpaceGB': get_virtual_disk_size(vm), - 'createDate': (vm.config.createDate + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S') if vm.runtime.bootTime else None, # 虚拟机的创建时间 - 'bootTime': (vm.runtime.bootTime + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S') if vm.runtime.bootTime else None, # 虚拟机上次启动的时间 'createDate': (vm.config.createDate + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'), # 虚拟机的创建时间 - # 'createDate': vm.config.createDate, # 虚拟机的创建时间 - # 'bootTime': vm.runtime.bootTime, # 虚拟机上次启动的时间 - 'snapshots': [], - 'Host': vm.runtime.host.name, - 'vmPath': vm.config.files.vmPathName - } - # 判断虚拟机是否有快照,如果有就构造快照结构并记录当前快照ID;如果没有就设为 None。 - if vm.snapshot: - current_snapshot = vm.snapshot.currentSnapshot - root_snapshots = vm.snapshot.rootSnapshotList - - for snapshot in root_snapshots: - vm_info['snapshots'].extend(build_snapshot_dict(snapshot)) - - vm_info['currentSnapshotId'] = ( - current_snapshot._moId if current_snapshot else None - ) - else: - vm_info['snapshots'] = None - vm_info['currentSnapshotId'] = None - - return vm_info - -def build_snapshot_dict(snapshot): - """递归构造快照结构""" 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, - 'quiesced': getattr(snapshot, 'quiesced', None), - 'children': [] + 'NodeHost': vm['NodeHost'], + 'VMName': vm['name'], + 'Snapshot Name': snapshot['name'], + 'Description': snapshot['description'], + 'CreateTime': snapshot['createTime'], + 'State': snapshot['state'], + 'ID': snapshot['id'], + 'MOID': snapshot['moId'], + 'Quiesced': snapshot['quiesced'], + 'is_old': is_old } - if snapshot.childSnapshotList: - for child in snapshot.childSnapshotList: - snapshot_info['children'].extend(build_snapshot_dict(child)) + # 如果是旧快照,添加到旧快照列表 + if is_old: + old_snapshots.append(snapshot_info) - return [snapshot_info] + snapshot_data.append(snapshot_info) + + for child in snapshot['children']: + collect_snapshot_data(child, vm, snapshot_data, old_snapshots) """设置表格样式""" @@ -135,104 +114,17 @@ def style_sheet(sheet, is_old_data=None): cell.fill = PatternFill(start_color='ADD8E6', end_color='ADD8E6', fill_type='solid') -"""获取虚拟机的总磁盘大小(仅是虚拟磁盘的分配空间)""" -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 collect_snapshot_data(snapshot, vm, snapshot_data, old_snapshots): - """递归扁平化快照数据用于Excel,并收集旧快照""" - create_time = datetime.strptime(snapshot['createTime'], '%Y-%m-%d %H:%M:%S') - is_old = create_time < (datetime.now() - timedelta(days=SNAPSHOT_RETENTION_DAYS)) - - snapshot_info = { - 'NodeHost': vm['NodeHost'], - 'VMName': vm['name'], - 'Snapshot Name': snapshot['name'], - 'Description': snapshot['description'], - 'CreateTime': snapshot['createTime'], - 'State': snapshot['state'], - 'ID': snapshot['id'], - 'MOID': snapshot['moId'], - 'Quiesced': snapshot['quiesced'], - 'is_old': is_old - } - - # 如果是旧快照,添加到旧快照列表 - if is_old: - old_snapshots.append(snapshot_info) - - snapshot_data.append(snapshot_info) - - for child in snapshot['children']: - collect_snapshot_data(child, vm, snapshot_data, old_snapshots) - - -# 输出数据到 Excel 文件 -def create_excel_report(vms): - vm_data = [] - snapshot_data = [] - old_snapshots = [] # 用于存储旧快照的信息 - - for vm in vms: - vm_data.append({ - 'NodeHost': vm['NodeHost'], - 'VMName': vm['name'], - 'MOID': vm['moId'], - 'PowerState': vm['powerState'], - 'System': vm['system'], - 'IPAddress': vm['ipAddress'], - 'HostName': vm['hostName'], - 'CurrentSnapshotID': vm.get('currentSnapshotId'), - 'DiskSpace/GB': vm['diskSpaceGB'], - 'createDate': vm['createDate'], # 虚拟机的创建时间 - 'bootTime': vm['bootTime'], # 虚拟机上次启动的时间 - 'Host': vm['Host'], - 'VMPath': vm['vmPath'] - }) - - if vm['snapshots']: - for snapshot in vm['snapshots']: - collect_snapshot_data(snapshot, vm, snapshot_data, old_snapshots) - - vm_df = pd.DataFrame(vm_data) - snapshot_df = pd.DataFrame(snapshot_data) - - with pd.ExcelWriter(EXCEL_OUTPUT_PATH, 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 - style_sheet(workbook['VMs']) - # style_sheet(workbook['Snapshots'], snapshot_df['is_old'].tolist()) - # 判断 DataFrame 是否有数据,没有数据则跳过,有数据则写入 excel 文件 - if 'is_old' in snapshot_df.columns and snapshot_df['is_old'].tolist(): - style_sheet(workbook['Snapshots'], snapshot_df['is_old'].tolist()) - - logger.debug(f"Excel 文件已生成: {EXCEL_OUTPUT_PATH}") - # 返回旧快照的数据 - return old_snapshots - - # 输出待删除的旧快照到 YAML 文件 def export_yaml(old_snapshots): - print(old_snapshots) - + logger.info(f"可删除的快照有 {len(old_snapshots)} 个") # 将旧快照信息存储到 YAML 文件 with open(YAML_OUTPUT_PATH, 'w', encoding='utf-8') as yaml_file: # allow_unicode:输出 Unicode 字符(中文等),allow_unicode:使用块样式(多行缩进),sort_keys:不按键名排序,保留原始插入顺序 yaml.dump(old_snapshots, yaml_file, allow_unicode=True, default_flow_style=False, sort_keys=False) logger.debug(f"YAML 文件已生成: {YAML_OUTPUT_PATH}") - if __name__ == '__main__': - vms = get_all_vms() # 主函数入口,获取虚拟机信息 - # print(vms) - old_snapshots = create_excel_report(vms) # 生成Excel报告并获取旧快照 + vms = get_all_vms() # 导出 excel 和 yaml 文件,需要先获取虚拟机信息 + old_snapshots = create_excel_report(vms) # 生成 Excel 报告并获取旧快照 export_yaml(old_snapshots) - # print(old_snapshots) \ No newline at end of file + # print(old_snapshots) diff --git a/core/get_vm_snapshots.py b/core/get_vm_snapshots.py new file mode 100644 index 0000000..1f4e23e --- /dev/null +++ b/core/get_vm_snapshots.py @@ -0,0 +1,100 @@ +from pyVmomi import vim +from datetime import timedelta +from pyVim.connect import Disconnect +from utils.logger import logger +from core.vm_connector import connect_vcenter +from config.settings import MANAGEMENT_NODES + + +def get_all_vms(): + """主函数,负责流程控制 """ + vm_list = [] + for node in MANAGEMENT_NODES: + si, vm_view = None, None # 初始两个变量值 + try: + # 1. 连接节点(核心逻辑不变) + si = connect_vcenter(node['host']) + content = si.RetrieveContent() + vm_view = content.viewManager.CreateContainerView( + content.rootFolder, [vim.VirtualMachine], True + ) + # 2. 收集VM信息 + for vm in vm_view.view: + vm_list.append(build_vm_info(vm, node['host'])) + except Exception as e: # 记录错误,跳过当前节点 + logger.error(f"处理节点 {node['host']} 失败:{e}") + finally: + # 无论成败,销毁视图+断开连接 + if vm_view: + vm_view.Destroy() + if si: + Disconnect(si) + logger.info(f"获取到 {len(vm_list)} 台虚拟机") + return vm_list + + +def build_vm_info(vm, node_host): + """构造虚拟机信息字典""" + vm_info = { + 'NodeHost': node_host, + 'name': vm.name, + 'moId': vm._moId, + 'powerState': vm.runtime.powerState, + 'system': vm.config.guestFullName, + 'ipAddress': vm.guest.ipAddress, + 'hostName': vm.guest.hostName, + 'diskSpaceGB': get_virtual_disk_size(vm), + 'createDate': (vm.config.createDate + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S') if vm.runtime.bootTime else None, # 虚拟机的创建时间 + 'bootTime': (vm.runtime.bootTime + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S') if vm.runtime.bootTime else None, # 虚拟机上次启动的时间 + 'snapshots': [], + 'Host': vm.runtime.host.name, + 'vmPath': vm.config.files.vmPathName + } + # 判断虚拟机是否有快照,如果有就构造快照结构并记录当前快照ID;如果没有就设为 None。 + if vm.snapshot: + current_snapshot = vm.snapshot.currentSnapshot + root_snapshots = vm.snapshot.rootSnapshotList + + for snapshot in root_snapshots: + vm_info['snapshots'].extend(build_snapshot_dict(snapshot)) + + vm_info['currentSnapshotId'] = ( + current_snapshot._moId if current_snapshot else None + ) + else: + vm_info['snapshots'] = None + vm_info['currentSnapshotId'] = None + return vm_info + + +def build_snapshot_dict(snapshot): + """递归构造快照结构""" + 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, + 'quiesced': getattr(snapshot, 'quiesced', None), + 'children': [] + } + if snapshot.childSnapshotList: + for child in snapshot.childSnapshotList: + snapshot_info['children'].extend(build_snapshot_dict(child)) + return [snapshot_info] + + +"""获取虚拟机的总磁盘大小(仅是虚拟磁盘的分配空间)""" +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 + + +if __name__ == '__main__': + vms = get_all_vms() # 主函数入口,获取虚拟机信息 + # print(vms) diff --git a/core/deleteSnapshots.py b/core/remove_snapshots.py similarity index 68% rename from core/deleteSnapshots.py rename to core/remove_snapshots.py index b36f149..92aa348 100644 --- a/core/deleteSnapshots.py +++ b/core/remove_snapshots.py @@ -1,25 +1,22 @@ -import yaml -import os, time +import os, time, yaml from pyVmomi import vim -from pyVim.connect import SmartConnect, Disconnect -from config.settings import MANAGEMENT_NODES,YAML_OUTPUT_PATH +from pyVim.connect import Disconnect +from core.vm_connector import connect_vcenter +from config.settings import YAML_OUTPUT_PATH from utils.logger import logger -def connect_vcenter(host): - """根据 NodeHost 连接对应 vCenter""" - for node in MANAGEMENT_NODES: - if node['host'] == host: - return SmartConnect( - host=node['host'], - user=node['user'], - pwd=node['password'], - disableSslCertValidation=True - ) - raise Exception(f"未找到 {host} 的连接信息") +def load_old_snapshots(file_path): + """从 YAML 文件中加载旧快照""" + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as yaml_file: + return yaml.safe_load(yaml_file) + else: + logger.error(f"{file_path} 文件不存在.") + return [] -def main(dele_snapshots): +def remove_snapshot(dele_snapshots): """根据 YAML 信息删除旧快照""" # print(dele_snapshots) if not dele_snapshots: @@ -31,52 +28,55 @@ def main(dele_snapshots): for snapshot in dele_snapshots: if snapshot['is_old']: grouped.setdefault(snapshot['NodeHost'], []).append(snapshot) - + # print(grouped) for host, snapshots in grouped.items(): - logger.info(f"连接到 {host} 删除快照...") - si = connect_vcenter(host) - + logger.info(f"连接 {host} 进行删除快照...") try: - for snapshot in snapshots: - delete_snapshot(si, snapshot) # 调用删除快照函数 - finally: - Disconnect(si) + si = connect_vcenter(host) + try: + for snapshot in snapshots: + # print(snapshot) + delete_snapshot(si, snapshot) # 调用删除快照函数 + finally: + Disconnect(si) + except Exception as e: + logger.error(f"快照删除失败,因为连接 {host} 失败:{e}" ) # 执行快照删除的(核心)函数 def delete_snapshot(si, snapshot_info): - """执行快照删除""" + """ 执行快照删除 """ content = si.RetrieveContent() snap_name = f"{snapshot_info['VMName']}-{snapshot_info['Snapshot Name']}-({snapshot_info['MOID']})" - - vm = find_vm_by_name(content, snapshot_info['VMName']) # VMName即根据获取到的虚拟机名称查找虚拟机是否存放 + vm = find_vm_by_name(content, snapshot_info['VMName']) # 根据快照名,查找出相应的虚拟机 + # print(snap_name,vm) if not vm: logger.info(f"未找到 VM: {snapshot_info['VMName']}") return # 检查该虚拟机是否有快照 if not vm.snapshot: - logger.warning(snap_name,":快照不存在") + logger.warning(f"{snap_name}:快照不存在") return - # 虚拟机的快照列表中找到具有指定 MOID 的快照对象 + # 调用函数获取虚拟机快照的 MOID 信息 snapshot_obj = find_snapshot_by_moid( vm.snapshot.rootSnapshotList, snapshot_info['MOID'] ) + # print(snapshot_obj) if not snapshot_obj: logger.warning(snap_name,":未找到") return - logger.info(f"正在删除 Snapshot: {snap_name}") try: - """删除快照核心代码,调用快照对象的 RemoveSnapshot_Task 方法执行。removeChildren = False:表示删除该快照时不删除其子快照。""" - task = snapshot_obj.RemoveSnapshot_Task(removeChildren=False) + logger.info(f"正在删除 Snapshot: {snap_name}") + task = snapshot_obj.RemoveSnapshot_Task(removeChildren=False) # 改进的等待逻辑 while task.info.state in [vim.TaskInfo.State.running, vim.TaskInfo.State.queued]: - time.sleep(1) # 避免 CPU 空转,每秒检查一次 + time.sleep(1) # 每秒检查一次 # 更完整的状态判断 if task.info.state == vim.TaskInfo.State.success: @@ -94,8 +94,8 @@ def delete_snapshot(si, snapshot_info): raise +"""根据 VM 名称查找虚拟机""" def find_vm_by_name(content, vm_name): - """根据 VM 名称查找虚拟机对象""" container = content.viewManager.CreateContainerView( content.rootFolder, [vim.VirtualMachine], True ) @@ -109,8 +109,8 @@ def find_vm_by_name(content, vm_name): return None +""" 递归查找 snapshot 对象 """ def find_snapshot_by_moid(snapshot_tree, moid): - """递归查找 snapshot 对象""" for snapshot in snapshot_tree: if snapshot.snapshot._moId == moid: return snapshot.snapshot @@ -122,16 +122,6 @@ def find_snapshot_by_moid(snapshot_tree, moid): return None -def load_old_snapshots(file_path): - """从 YAML 文件中加载旧快照""" - if os.path.exists(file_path): - with open(file_path, 'r', encoding='utf-8') as yaml_file: - return yaml.safe_load(yaml_file) - else: - logger.error(f"{file_path} 文件不存在.") - return [] - - if __name__ == "__main__": - old_snapshots = load_old_snapshots(YAML_OUTPUT_PATH) - main(old_snapshots) \ No newline at end of file + old_snapshots = load_old_snapshots(YAML_OUTPUT_PATH) # 获取待删除的快照信息 + remove_snapshot(old_snapshots) \ No newline at end of file diff --git a/core/vm_connector.py b/core/vm_connector.py new file mode 100644 index 0000000..7915047 --- /dev/null +++ b/core/vm_connector.py @@ -0,0 +1,36 @@ +import ssl +from pyVim.connect import SmartConnect, Disconnect +from config.settings import MANAGEMENT_NODES +from utils.logger import logger # 复用你之前的日志配置 + + +def connect_vcenter(host): + """ + 根据NodeHost连接对应vCenter/ESXi + :param host: 节点IP/主机名 + :return: ServiceInstance对象 + :raise: Exception(连接失败/未找到配置) + """ + # 遍历全局配置的MANAGEMENT_NODES + for node in MANAGEMENT_NODES: + if node['host'] == host: + context = ssl._create_unverified_context() # 禁用SSL验证 + si = SmartConnect( + host=node['host'], + user=node['user'], + pwd=node['password'], + sslContext=context # 规范的SSL配置 + ) + if not si: + raise Exception(f"{host}连接成功但未返回ServiceInstance") + logger.info(f"成功连接到节点: {host}") + return si + + # 匹配不到 host 主动抛异常 + logger.error(f"未找到节点 {host} 的连接信息") + raise Exception(f"未找到 {host} 的连接信息") + + +if __name__ == "__main__": + si = connect_vcenter(MANAGEMENT_NODES[0]['host']) + print(si) \ No newline at end of file diff --git a/main.py b/main.py index 3ce71a8..e13db24 100644 --- a/main.py +++ b/main.py @@ -1,20 +1,59 @@ -# 这是一个示例 Python 脚本。 - -# 按 Shift+F10 执行或将其替换为您的代码。 -# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。 +import time +from apscheduler.schedulers.background import BackgroundScheduler +from utils.logger import logger +from config.settings import YAML_OUTPUT_PATH +from core.get_vm_snapshots import get_all_vms +from core.data_exporter import create_excel_report, export_yaml +from core.remove_snapshots import load_old_snapshots, remove_snapshot -def print_hi(name): - # 在下面的代码行中使用断点来调试脚本。 - print(f'Hi, {name}') # 按 Ctrl+F8 切换断点。 +def export_files(): + """导出Excel和Yaml文件的函数""" + logger.info("🔍 开始收集VM和快照信息...") + vms = get_all_vms() # 主函数入口,获取虚拟机信息 + + # 导出Excel报表 + logger.info("📝 开始导出Excel报表...") + old_snapshots = create_excel_report(vms) # 生成Excel报告并获取旧快照 + + # 导出Yaml文件 + logger.info("📝 开始导出 Yaml 文件...") + export_yaml(old_snapshots) + logger.info("========== Excel和Yaml文件导出完成 ==========") -# 按装订区域中的绿色按钮以运行脚本。 -if __name__ == '__main__': - print_hi('PyCharm') - -# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助 +def delete_old_snapshots(): + """删除旧快照的函数""" + logger.info("🗑️ 开始删除过旧快照...") + old_snapshots = load_old_snapshots(YAML_OUTPUT_PATH) + remove_snapshot(old_snapshots) + logger.info("========== VM快照清理任务执行完成 ==========") +def main(): + """主执行函数""" + + # 设置定时任务 + scheduler = BackgroundScheduler() + + # 每周六凌晨4点导出Excel和Yaml文件 + scheduler.add_job(export_files, 'cron', day_of_week='sat', hour=4, minute=0) + + # 每周六晚上7点执行删除快照任务 + scheduler.add_job(delete_old_snapshots, 'cron', day_of_week='sat', hour=19, minute=0) + + scheduler.start() + + logger.info("定时任务已设置:每周六凌晨4点导出文件,晚上7点删除快照") + + try: + # 保持主程序运行,以便调度器能正常工作 + while True: + time.sleep(1) + + except (KeyboardInterrupt, SystemExit): + scheduler.shutdown() +if __name__ == "__main__": + main() diff --git a/utils/logger.py b/utils/logger.py index 5f7c4d8..55e1349 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -1,7 +1,11 @@ -import logging -# from config.settings import LOG_FILE_PATH +import os, logging from datetime import datetime +# 获取项目根目录(假设 logger.py 在 utils/ 目录下) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LOG_DIR = os.path.join(BASE_DIR, 'logs') + + def get_logger(): """配置日志系统,返回logger实例""" # 创建logger @@ -15,8 +19,16 @@ def get_logger(): # 定义日志格式 formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + # 自动创建日志目录 + os.makedirs(LOG_DIR, exist_ok=True) + # 日志文件路径 + log_file = os.path.join( + LOG_DIR, + f'{datetime.now().strftime("%Y-%m-%d")}-vm_snapshot_cleanup.log' + ) + # 文件处理器(写入日志文件) - file_handler = logging.FileHandler(f'D:\\PycharmProjects\\RemoveWeeklyShapshot\\logs\\{datetime.now().strftime('%Y%m%d')}-RemoveWeeklyShapshot.log', encoding='utf-8') + file_handler = logging.FileHandler(log_file, encoding='utf-8') file_handler.setFormatter(formatter) # 应用格式化器 # 控制台处理器(输出到终端)