接近完善的代码,待生产环境测试。

This commit is contained in:
panjunlan
2026-02-21 14:26:14 +08:00
parent 9024e9c8e4
commit 17e9e0c3bc
11 changed files with 357 additions and 293 deletions

14
.env
View File

@@ -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

View File

@@ -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
```

View File

@@ -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
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默认自签证书

View File

@@ -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__":
# 打印全局配置

View File

@@ -1,2 +0,0 @@
def vcenter_connector():
return None

View File

@@ -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)

100
core/get_vm_snapshots.py Normal file
View File

@@ -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)

View File

@@ -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)
old_snapshots = load_old_snapshots(YAML_OUTPUT_PATH) # 获取待删除的快照信息
remove_snapshot(old_snapshots)

36
core/vm_connector.py Normal file
View File

@@ -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)

63
main.py
View File

@@ -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()

View File

@@ -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) # 应用格式化器
# 控制台处理器(输出到终端)