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

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 # 项目描述 │ README.md # 项目描述
├─config # 项目程序配置文件 ├─config # 项目程序配置文件
│ │ config.yaml │ │ config.yaml # 配置文件
│ │ settings.py │ │ settings.py # 配置加载和全局变量
├─core # 核心程序 ├─core # 核心程序
│ │ deleteSnapshots.py │ │ deleteSnapshots.py
@@ -414,8 +414,8 @@ D:.
│ 2026-02-20-old_snapshots.yaml │ 2026-02-20-old_snapshots.yaml
│ 2026-02-20_20-36-45-VMsSnapShots_report.xlsx │ 2026-02-20_20-36-45-VMsSnapShots_report.xlsx
├─utils # 日志输出格式设置 ├─utils # 工具函数
│ │ logger.py │ │ logger.py # 日志配置
``` ```
@@ -423,11 +423,27 @@ D:.
## 所用到的 Python 库 ## 所用到的 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 ``` 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 # 管理节点配置包含vCenter和ESXi
management_nodes: management_nodes:
# vCenter节点 # vCenter节点
- type: vcenter # 标记类型为vcenter # - type: vcenter # 标记类型为vcenter
name: vc1 # 节点名称(用于日志) # name: vc1 # 节点名称(用于日志)
host: 192.168.40.134 # 地址 # host: 192.168.40.134 # 地址
user: administrator@lan.com # user: administrator@lan.com
password: Root@2025 # password: Root@2025
max_delete_concurrent: 4 # 该节点最大并发删除数 # max_delete_concurrent: 4 # 该节点最大并发删除数
# ESXi节点未接入 vCenter 的 Esxi 主机) # ESXi节点未接入 vCenter 的 Esxi 主机)
# - type: esxi # 标记类型为esxi - type: esxi # 标记类型为esxi
# name: esxi1 name: esxi1
# host: 192.168.40.133 host: 192.168.40.133
# user: root # ESXi默认用root user: root # ESXi默认用root
# password: Root@2025 password: Root@2025
# max_delete_concurrent: 2 # ESXi性能较弱并发数可设小些 max_delete_concurrent: 2 # ESXi性能较弱并发数可设小些
# - type: esxi - type: esxi
# name: esxi2 name: esxi2
# host: 192.168.40.135 host: 192.168.40.135
# user: root user: root
# password: Root@2025 password: Root@2025
# max_delete_concurrent: 2 max_delete_concurrent: 2
# 全局策略配置 # 全局策略配置
global: global:
snapshot_retention_days: 0 # 可选,使用默认值 15 天 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
# '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默认自签证书 # ESXi连接特殊配置禁用SSL验证ESXi默认自签证书
disable_ssl_verify: true

View File

@@ -1,8 +1,8 @@
import yaml import yaml, os
import os
from datetime import datetime from datetime import datetime
from utils.logger import logger from utils.logger import logger
def load_config(): def load_config():
"""加载 YAML 配置文件并解析其内容""" """加载 YAML 配置文件并解析其内容"""
try: try:
@@ -17,10 +17,9 @@ def load_config():
"SNAPSHOT_RETENTION_DAYS": int(global_config.get('snapshot_retention_days', 15)), "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'), # "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'), # "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'), "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\\RemoveWeeklyShapshot\\output\\{datetime.now().strftime('%Y-%m-%d')}-old_snapshots.yaml'), "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), "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"] #LOG_FILE_PATH = config["LOG_FILE_PATH"]
DISABLE_SSL_VERIFY = config["DISABLE_SSL_VERIFY"] DISABLE_SSL_VERIFY = config["DISABLE_SSL_VERIFY"]
# 验证配置函数
def validate_config():
pass # 加载时已验证,此处留空
if __name__ == "__main__": 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 import yaml
from pyVmomi import vim import pandas as pd
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pyVim.connect import SmartConnect, Disconnect
from openpyxl.styles import Border, Side, Font, PatternFill from openpyxl.styles import Border, Side, Font, PatternFill
from utils.logger import logger 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(): def collect_snapshot_data(snapshot, vm, snapshot_data, old_snapshots):
""" 主函数,负责流程控制""" """ 递归快照数据用于 Excel并收集旧快照 """
vm_list = [] 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 = { snapshot_info = {
'name': snapshot.name, 'NodeHost': vm['NodeHost'],
'description': snapshot.description, 'VMName': vm['name'],
'createTime': (snapshot.createTime + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S'), 'Snapshot Name': snapshot['name'],
'state': str(snapshot.state), 'Description': snapshot['description'],
'id': snapshot.id, 'CreateTime': snapshot['createTime'],
'moId': snapshot.snapshot._moId, 'State': snapshot['state'],
'quiesced': getattr(snapshot, 'quiesced', None), 'ID': snapshot['id'],
'children': [] 'MOID': snapshot['moId'],
'Quiesced': snapshot['quiesced'],
'is_old': is_old
} }
if snapshot.childSnapshotList: # 如果是旧快照,添加到旧快照列表
for child in snapshot.childSnapshotList: if is_old:
snapshot_info['children'].extend(build_snapshot_dict(child)) 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') 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 文件 # 输出待删除的旧快照到 YAML 文件
def export_yaml(old_snapshots): def export_yaml(old_snapshots):
print(old_snapshots) logger.info(f"可删除的快照有 {len(old_snapshots)}")
# 将旧快照信息存储到 YAML 文件 # 将旧快照信息存储到 YAML 文件
with open(YAML_OUTPUT_PATH, 'w', encoding='utf-8') as yaml_file: with open(YAML_OUTPUT_PATH, 'w', encoding='utf-8') as yaml_file:
# allow_unicode输出 Unicode 字符中文等allow_unicode使用块样式多行缩进sort_keys不按键名排序保留原始插入顺序 # allow_unicode输出 Unicode 字符中文等allow_unicode使用块样式多行缩进sort_keys不按键名排序保留原始插入顺序
yaml.dump(old_snapshots, yaml_file, allow_unicode=True, default_flow_style=False, sort_keys=False) yaml.dump(old_snapshots, yaml_file, allow_unicode=True, default_flow_style=False, sort_keys=False)
logger.debug(f"YAML 文件已生成: {YAML_OUTPUT_PATH}") logger.debug(f"YAML 文件已生成: {YAML_OUTPUT_PATH}")
if __name__ == '__main__': if __name__ == '__main__':
vms = get_all_vms() # 主函数入口,获取虚拟机信息 vms = get_all_vms() # 导出 excel 和 yaml 文件,需要先获取虚拟机信息
# print(vms)
old_snapshots = create_excel_report(vms) # 生成 Excel 报告并获取旧快照 old_snapshots = create_excel_report(vms) # 生成 Excel 报告并获取旧快照
export_yaml(old_snapshots) export_yaml(old_snapshots)
# print(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, yaml
import os, time
from pyVmomi import vim from pyVmomi import vim
from pyVim.connect import SmartConnect, Disconnect from pyVim.connect import Disconnect
from config.settings import MANAGEMENT_NODES,YAML_OUTPUT_PATH from core.vm_connector import connect_vcenter
from config.settings import YAML_OUTPUT_PATH
from utils.logger import logger from utils.logger import logger
def connect_vcenter(host): def load_old_snapshots(file_path):
"""根据 NodeHost 连接对应 vCenter""" """从 YAML 文件中加载旧快照"""
for node in MANAGEMENT_NODES: if os.path.exists(file_path):
if node['host'] == host: with open(file_path, 'r', encoding='utf-8') as yaml_file:
return SmartConnect( return yaml.safe_load(yaml_file)
host=node['host'], else:
user=node['user'], logger.error(f"{file_path} 文件不存在.")
pwd=node['password'], return []
disableSslCertValidation=True
)
raise Exception(f"未找到 {host} 的连接信息")
def main(dele_snapshots): def remove_snapshot(dele_snapshots):
"""根据 YAML 信息删除旧快照""" """根据 YAML 信息删除旧快照"""
# print(dele_snapshots) # print(dele_snapshots)
if not dele_snapshots: if not dele_snapshots:
@@ -31,16 +28,19 @@ def main(dele_snapshots):
for snapshot in dele_snapshots: for snapshot in dele_snapshots:
if snapshot['is_old']: if snapshot['is_old']:
grouped.setdefault(snapshot['NodeHost'], []).append(snapshot) grouped.setdefault(snapshot['NodeHost'], []).append(snapshot)
# print(grouped)
for host, snapshots in grouped.items(): for host, snapshots in grouped.items():
logger.info(f"连接 {host} 删除快照...") logger.info(f"连接 {host} 进行删除快照...")
try:
si = connect_vcenter(host) si = connect_vcenter(host)
try: try:
for snapshot in snapshots: for snapshot in snapshots:
# print(snapshot)
delete_snapshot(si, snapshot) # 调用删除快照函数 delete_snapshot(si, snapshot) # 调用删除快照函数
finally: finally:
Disconnect(si) Disconnect(si)
except Exception as e:
logger.error(f"快照删除失败,因为连接 {host} 失败:{e}" )
# 执行快照删除的(核心)函数 # 执行快照删除的(核心)函数
@@ -48,35 +48,35 @@ def delete_snapshot(si, snapshot_info):
""" 执行快照删除 """ """ 执行快照删除 """
content = si.RetrieveContent() content = si.RetrieveContent()
snap_name = f"{snapshot_info['VMName']}-{snapshot_info['Snapshot Name']}-({snapshot_info['MOID']})" snap_name = f"{snapshot_info['VMName']}-{snapshot_info['Snapshot Name']}-({snapshot_info['MOID']})"
vm = find_vm_by_name(content, snapshot_info['VMName']) # 根据快照名,查找出相应的虚拟机
vm = find_vm_by_name(content, snapshot_info['VMName']) # VMName即根据获取到的虚拟机名称查找虚拟机是否存放 # print(snap_name,vm)
if not vm: if not vm:
logger.info(f"未找到 VM: {snapshot_info['VMName']}") logger.info(f"未找到 VM: {snapshot_info['VMName']}")
return return
# 检查该虚拟机是否有快照 # 检查该虚拟机是否有快照
if not vm.snapshot: if not vm.snapshot:
logger.warning(snap_name,":快照不存在") logger.warning(f"{snap_name}:快照不存在")
return return
# 虚拟机快照列表中找到具有指定 MOID 的快照对象 # 调用函数获取虚拟机快照 MOID 信息
snapshot_obj = find_snapshot_by_moid( snapshot_obj = find_snapshot_by_moid(
vm.snapshot.rootSnapshotList, vm.snapshot.rootSnapshotList,
snapshot_info['MOID'] snapshot_info['MOID']
) )
# print(snapshot_obj)
if not snapshot_obj: if not snapshot_obj:
logger.warning(snap_name,":未找到") logger.warning(snap_name,":未找到")
return return
logger.info(f"正在删除 Snapshot: {snap_name}")
try: try:
"""删除快照核心代码,调用快照对象的 RemoveSnapshot_Task 方法执行。removeChildren = False表示删除该快照时不删除其子快照。""" """删除快照核心代码,调用快照对象的 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]: 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: if task.info.state == vim.TaskInfo.State.success:
@@ -94,8 +94,8 @@ def delete_snapshot(si, snapshot_info):
raise raise
"""根据 VM 名称查找虚拟机"""
def find_vm_by_name(content, vm_name): def find_vm_by_name(content, vm_name):
"""根据 VM 名称查找虚拟机对象"""
container = content.viewManager.CreateContainerView( container = content.viewManager.CreateContainerView(
content.rootFolder, [vim.VirtualMachine], True content.rootFolder, [vim.VirtualMachine], True
) )
@@ -109,8 +109,8 @@ def find_vm_by_name(content, vm_name):
return None return None
def find_snapshot_by_moid(snapshot_tree, moid):
""" 递归查找 snapshot 对象 """ """ 递归查找 snapshot 对象 """
def find_snapshot_by_moid(snapshot_tree, moid):
for snapshot in snapshot_tree: for snapshot in snapshot_tree:
if snapshot.snapshot._moId == moid: if snapshot.snapshot._moId == moid:
return snapshot.snapshot return snapshot.snapshot
@@ -122,16 +122,6 @@ def find_snapshot_by_moid(snapshot_tree, moid):
return None 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__": if __name__ == "__main__":
old_snapshots = load_old_snapshots(YAML_OUTPUT_PATH) old_snapshots = load_old_snapshots(YAML_OUTPUT_PATH) # 获取待删除的快照信息
main(old_snapshots) 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 脚本。 import time
from apscheduler.schedulers.background import BackgroundScheduler
# 按 Shift+F10 执行或将其替换为您的代码。 from utils.logger import logger
# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。 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): def export_files():
# 在下面的代码行中使用断点来调试脚本。 """导出Excel和Yaml文件的函数"""
print(f'Hi, {name}') # 按 Ctrl+F8 切换断点。 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文件导出完成 ==========")
# 按装订区域中的绿色按钮以运行脚本。 def delete_old_snapshots():
if __name__ == '__main__': """删除旧快照的函数"""
print_hi('PyCharm') logger.info("🗑️ 开始删除过旧快照...")
old_snapshots = load_old_snapshots(YAML_OUTPUT_PATH)
# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助 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 import os, logging
# from config.settings import LOG_FILE_PATH
from datetime import datetime 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(): def get_logger():
"""配置日志系统返回logger实例""" """配置日志系统返回logger实例"""
# 创建logger # 创建logger
@@ -15,8 +19,16 @@ def get_logger():
# 定义日志格式 # 定义日志格式
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 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) # 应用格式化器 file_handler.setFormatter(formatter) # 应用格式化器
# 控制台处理器(输出到终端) # 控制台处理器(输出到终端)