Compare commits

13 Commits
main ... Dev

Author SHA1 Message Date
panjunlan
1af0411783 更新README文件 2026-02-22 16:24:29 +08:00
panjunlan
e8ed320193 main 程序添加启动调度器 2026-02-22 15:44:10 +08:00
panjunlan
375a54187d 修改可在config文件自定义运行时间 2026-02-22 14:44:52 +08:00
panjunlan
e505148026 修改README文件 2026-02-22 11:19:31 +08:00
panjunlan
019c009700 增加自动创建logs和output目录 2026-02-22 10:00:44 +08:00
panjunlan
26678accb7 更新README文件 2026-02-21 16:13:16 +08:00
panjunlan
d462263b39 接近完善的代码,待生产环境测试。 2026-02-21 15:00:11 +08:00
panjunlan
7c95ccb2af 接近完善的代码,待生产环境测试。 2026-02-21 14:27:57 +08:00
panjunlan
17e9e0c3bc 接近完善的代码,待生产环境测试。 2026-02-21 14:26:14 +08:00
panjunlan
9024e9c8e4 添加了删除快照的功能 2026-02-20 21:36:58 +08:00
panjunlan
7a0a51be4e 未添加删除快照逻辑 2026-02-20 16:55:53 +08:00
panjunlan
199cbab4aa Get Vms Snapshots and Export Excel correct info. 2026-02-19 23:23:51 +08:00
panjunlan
3332e3b865 Get Vms Snapshots and Export Excel has completed. 2026-02-19 15:35:27 +08:00
12 changed files with 1242 additions and 100 deletions

14
.env
View File

@@ -1,14 +0,0 @@
# vCenter配置支持多个
VCENTER_HOSTS=192.168.40.134
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/log/vm_snapshot_cleanup.log
LOG_FILE_PATH=./log/vm_snapshot_cleanup.log

620
README.md
View File

@@ -1,7 +1,613 @@
# 以下需求需要每个月执行一次使用ansible实现还是使用python代码实行好
-[x] 获取所有vms
-[ ] 获取所有snapshots
-[ ] 筛选出15天半个月前的snapshots
-[ ] 以上内容以Excel表格的形式导出
-[ ] 最后删除15天前的snapshot并同时记录删除的snapshot日志信息
-[ ] 需要控制每台vCenter不可以同时删除超过4个快照
# RemoveWeeklySnapshot
Vmware 虚拟机自动化程序:自动化导出虚拟机和快照信息,自动化删除旧快照。
## Todo List
- [x] Yaml 文件保存配置信息
- [x] 通过 Yaml 文件信息连接vCenter/Esxi
- [x] 文件获取所有 vms
- [x] 获取所有 snapshots
- [x] 筛选出15天半个月前的 snapshots
- [x] 以上内容以 Excel 表格的形式导出,超出 15 天的快照蓝色底填充标识
- [x] 最后删除 15 天前的 snapshot并同时记录删除的 snapshot 日志信息
- [x] 设置计划任务,每周六(或 每15 天)执行一次
- [ ] 增加排除不能删除的 snapshots 信息,用红色底填充标识
- [ ] 删除快照前/后发送邮件通知
- [ ] 多线程删除(控制每台 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、快照、存储和其他资源。
>
>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 实时性能数据
>
>1. **AcquireMksTicket / AcquireTicket**:获取访问虚拟机控制台的票据,允许用户通过 Web 界面访问 VM。
>2. **Clone / CloneVM_Task**:克隆一个虚拟机,创建其副本。
>3. **CreateSnapshot / CreateSnapshot_Task**:创建虚拟机快照,以保存当前 VM 的状态。
>4. **Destroy / Destroy_Task**:删除虚拟机及其所有数据。
>5. **PowerOn / PowerOnVM_Task**:启动虚拟机。
>6. **PowerOff / PowerOffVM_Task**:关闭虚拟机。
>7. **Reset / ResetVM_Task**:重置虚拟机,相当于按下重启按钮。
>8. **RevertToCurrentSnapshot / RevertToCurrentSnapshot_Task**:将虚拟机恢复到当前快照的状态。
>9. **Migrate / MigrateVM_Task**:迁移虚拟机到不同的主机或数据存储。
>10. **SetCustomValue**:设置自定义属性值,以便在虚拟机上存储额外信息。
>
>### 属性列表
>
>这些属性通常包含关于虚拟机或其他资源的状态和配置信息。以下是一些关键属性的说明:
>
>1. **name**:虚拟机的名称。
>2. **guest**:关于虚拟机操作系统的信息,如操作系统类型、版本等。
>3. **config**:虚拟机的配置详情,例如 CPU、内存、硬盘等。
>4. **runtime**:虚拟机的运行时状态,包括电源状态(开机、关机、挂起等)。
>5. **snapshot**:当前虚拟机的快照信息。
>6. **summary**:虚拟机的概述信息,包括状态、资源使用情况等。
>7. **resourcePool**:虚拟机所在的资源池。
>8. **datastore**:虚拟机的存储位置,指向其使用的存储库。
>9. **overallStatus**:虚拟机的总体状态,可能是正常、警告、错误等。
>10. **recentTask**:最近执行的任务列表。
>
>### 其他属性
>
>- **capability**:虚拟机支持的功能,如支持的虚拟硬件版本。
>- **declaredAlarmState**:声明的警报状态,用于监控虚拟机的健康状况。
>- **triggeredAlarmState**:触发的警报状态,显示当前激活的警报。
```python
for vm in vm_view.view:
print(dir(vm)) # 输出所有可用的属性和方法
# print(vm.summary)
# print(vm.snapshot)
# print(vars(vm.summary))
```
``` json
['AcquireMksTicket', 'AcquireTicket', 'Answer', 'AnswerVM', 'ApplyEvcMode', 'ApplyEvcModeVM_Task', 'Array', 'AttachDisk', 'AttachDisk_Task', 'CheckCustomizationSpec', 'Clone', 'CloneVM_Task', 'ConsolidateDisks', 'ConsolidateVMDisks_Task', 'CreateScreenshot', 'CreateScreenshot_Task', 'CreateSecondary', 'CreateSecondaryEx', 'CreateSecondaryVMEx_Task', 'CreateSecondaryVM_Task', 'CreateSnapshot', 'CreateSnapshotEx', 'CreateSnapshotEx_Task', 'CreateSnapshot_Task', 'CryptoUnlock', 'CryptoUnlock_Task', 'Customize', 'CustomizeVM_Task', 'DefragmentAllDisks', 'Destroy', 'Destroy_Task', 'DetachDisk', 'DetachDisk_Task', 'DisableSecondary', 'DisableSecondaryVM_Task', 'DropConnections', 'EnableSecondary', 'EnableSecondaryVM_Task', 'EstimateStorageForConsolidateSnapshots_Task', 'EstimateStorageRequirementForConsolidate', 'ExportVm', 'ExtractOvfEnvironment', 'InstantClone', 'InstantClone_Task', 'MakePrimary', 'MakePrimaryVM_Task', 'MarkAsTemplate', 'MarkAsVirtualMachine', 'Migrate', 'MigrateVM_Task', 'MountToolsInstaller', 'PowerOff', 'PowerOffVM_Task', 'PowerOn', 'PowerOnVM_Task', 'PromoteDisks', 'PromoteDisks_Task', 'PutUsbScanCodes', 'QueryChangedDiskAreas', 'QueryConnections', 'QueryFaultToleranceCompatibility', 'QueryFaultToleranceCompatibilityEx', 'QueryUnownedFiles', 'RebootGuest', 'ReconfigVM_Task', 'Reconfigure', 'RefreshStorageInfo', 'Reload', 'ReloadFromPath', 'Relocate', 'RelocateVM_Task', 'RemoveAllSnapshots', 'RemoveAllSnapshots_Task', 'Rename', 'Rename_Task', 'Reset', 'ResetGuestInformation', 'ResetVM_Task', 'RevertToCurrentSnapshot', 'RevertToCurrentSnapshot_Task', 'SendNMI', 'SetCustomValue', 'SetDisplayTopology', 'SetScreenResolution', 'ShutdownGuest', 'StandbyGuest', 'StartRecording', 'StartRecording_Task', 'StartReplaying', 'StartReplaying_Task', 'StopRecording', 'StopRecording_Task', 'StopReplaying', 'StopReplaying_Task', 'Suspend', 'SuspendVM_Task', 'Terminate', 'TerminateFaultTolerantVM', 'TerminateFaultTolerantVM_Task', 'TerminateVM', 'TurnOffFaultTolerance', 'TurnOffFaultToleranceForVM_Task', 'UnmountToolsInstaller', 'Unregister', 'UnregisterVM', 'UpgradeTools', 'UpgradeTools_Task', 'UpgradeVM_Task', 'UpgradeVirtualHardware', '_GetMethodInfo', '_GetMethodList', '_GetMoId', '_GetPropertyInfo', '_GetPropertyList', '_GetServerGuid', '_GetStub', '_InvokeAccessor', '_InvokeMethod', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', '_methodInfo', '_moId', '_propInfo', '_propList', '_serverGuid', '_stub', '_version', '_wsdlName', 'alarmActionsEnabled', 'availableField', 'capability', 'config', 'configIssue', 'configStatus', 'customValue', 'datastore', 'declaredAlarmState', 'disabledMethod', 'effectiveRole', 'environmentBrowser', 'guest', 'guestHeartbeatStatus', 'layout', 'layoutEx', 'name', 'network', 'overallStatus', 'parent', 'parentVApp', 'permission', 'recentTask', 'reloadVirtualMachineFromPath_Task', 'resourceConfig', 'resourcePool', 'rootSnapshot', 'runtime', 'setCustomValue', 'snapshot', 'storage', 'summary', 'tag', 'triggeredAlarmState', 'value']
```
### 获取虚拟机属性
获取和打印出虚拟机 (VM) 概要信息的所有属性,提供了关于虚拟机的基本信息,`vm.summary` 会返回一个字典,包含常见属性。可以使用 vm.guest.ipAddress 方法获取到虚拟机的IP地址等。
```python
print(vm.summary)
```
> (vim.vm.Summary) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> vm = 'vim.VirtualMachine:1',
> runtime = (vim.vm.RuntimeInfo) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> device = (vim.vm.DeviceRuntimeInfo) [
> (vim.vm.DeviceRuntimeInfo) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> runtimeState = (vim.vm.DeviceRuntimeInfo.VirtualEthernetCardRuntimeState) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> vmDirectPathGen2Active = false,
> vmDirectPathGen2InactiveReasonVm = (str) [
> 'vmNptDisabledOrDisconnectedAdapter'
> ],
> vmDirectPathGen2InactiveReasonOther = (str) [],
> vmDirectPathGen2InactiveReasonExtended = <unset>,
> uptv2Active = <unset>,
> uptv2InactiveReasonVm = (str) [],
> uptv2InactiveReasonOther = (str) [],
> reservationStatus = <unset>,
> attachmentStatus = 'red',
> featureRequirement = (vim.vm.FeatureRequirement) []
> },
> key = 4000
> }
> ],
> host = 'vim.HostSystem:ha-host',
> connectionState = 'connected',
> powerState = 'poweredOff',
> vmFailoverInProgress = <unset>,
> faultToleranceState = 'notConfigured',
> dasVmProtection = <unset>,
> toolsInstallerMounted = false,
> suspendTime = <unset>,
> bootTime = <unset>,
> suspendInterval = 0,
> question = <unset>,
> memoryOverhead = <unset>,
> maxCpuUsage = 5184,
> maxMemoryUsage = 4096,
> numMksConnections = 0,
> recordReplayState = 'inactive',
> cleanPowerOff = false,
> needSecondaryReason = <unset>,
> onlineStandby = false,
> minRequiredEVCModeKey = <unset>,
> consolidationNeeded = false,
> offlineFeatureRequirement = (vim.vm.FeatureRequirement) [
> (vim.vm.FeatureRequirement) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> key = 'cpuid.lm',
> featureName = 'cpuid.lm',
> value = 'Bool:Min:1'
> }
> ],
> featureRequirement = (vim.vm.FeatureRequirement) [],
> featureMask = (vim.host.FeatureMask) [],
> vFlashCacheAllocation = <unset>,
> paused = false,
> snapshotInBackground = false,
> quiescedForkParent = <unset>,
> instantCloneFrozen = false,
> cryptoState = <unset>,
> suspendedToMemory = <unset>,
> opNotificationTimeout = <unset>,
> iommuActive = <unset>
> },
> guest = (vim.vm.Summary.GuestSummary) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> guestId = <unset>,
> guestFullName = <unset>,
> toolsStatus = 'toolsNotRunning',
> toolsVersionStatus = 'guestToolsUnmanaged',
> toolsVersionStatus2 = 'guestToolsUnmanaged',
> toolsRunningStatus = 'guestToolsNotRunning',
> hostName = <unset>,
> ipAddress = <unset>,
> hwVersion = <unset>
> },
> config = (vim.vm.Summary.ConfigSummary) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> name = 'VMware vCenter Server Appliance',
> template = false,
> vmPathName = '[datastore1] VMware vCenter Server Appliance/VMware vCenter Server Appliance.vmx',
> memorySizeMB = 4096,
> cpuReservation = 0,
> memoryReservation = 0,
> numCpu = 2,
> numEthernetCards = 1,
> numVirtualDisks = 13,
> uuid = '564d7d77-f37f-64a7-47f2-a131a337c070',
> instanceUuid = '529e8be2-58ca-617c-ad7e-7f66de667471',
> guestId = 'other3xLinux64Guest',
> guestFullName = 'Other 3.x Linux (64-bit)',
> annotation = 'VMware vCenter Server Appliance',
> product = <unset>,
> installBootRequired = <unset>,
> ftInfo = <unset>,
> managedBy = <unset>,
> tpmPresent = false,
> numVmiopBackings = 0,
> hwVersion = <unset>
> },
> storage = (vim.vm.Summary.StorageSummary) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> committed = 37476296711,
> uncommitted = 303123661815,
> unshared = 37476296711,
> timestamp = 2026-02-19T09:11:27.701184Z
> },
> quickStats = (vim.vm.Summary.QuickStats) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> overallCpuUsage = <unset>,
> overallCpuDemand = <unset>,
> overallCpuReadiness = <unset>,
> guestMemoryUsage = <unset>,
> hostMemoryUsage = <unset>,
> guestHeartbeatStatus = 'gray',
> distributedCpuEntitlement = <unset>,
> distributedMemoryEntitlement = <unset>,
> staticCpuEntitlement = <unset>,
> staticMemoryEntitlement = <unset>,
> grantedMemory = <unset>,
> privateMemory = <unset>,
> sharedMemory = <unset>,
> swappedMemory = <unset>,
> balloonedMemory = <unset>,
> consumedOverheadMemory = <unset>,
> ftLogBandwidth = <unset>,
> ftSecondaryLatency = <unset>,
> ftLatencyStatus = <unset>,
> compressedMemory = <unset>,
> uptimeSeconds = <unset>,
> ssdSwappedMemory = <unset>,
> activeMemory = <unset>,
> memoryTierStats = (vim.vm.Summary.QuickStats.MemoryTierStats) []
> },
> overallStatus = 'green',
> customValue = (vim.CustomFieldsManager.Value) []
> }
### 获取快照属性
输出快照所有信息
```python
print(vm.snapshot)
```
>(vim.vm.SnapshotInfo) {
>dynamicType = <unset>,
>dynamicProperty = (vmodl.DynamicProperty) [],
>currentSnapshot = 'vim.vm.Snapshot:1-snapshot-3',
>rootSnapshotList = (vim.vm.SnapshotTree) [
>(vim.vm.SnapshotTree) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> snapshot = 'vim.vm.Snapshot:1-snapshot-1',
> vm = 'vim.VirtualMachine:1',
> name = 'snap-01', # 第一层快照
> description = 'Ansible snapshot',
> id = 1,
> createTime = 2026-02-17T03:26:08.834505Z,
> state = 'poweredOff',
> quiesced = false,
> backupManifest = <unset>,
> childSnapshotList = (vim.vm.SnapshotTree) [
> (vim.vm.SnapshotTree) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> snapshot = 'vim.vm.Snapshot:1-snapshot-2',
> vm = 'vim.VirtualMachine:1',
> name = 'snap-02', # 第二层快照
> description = 'test-Ansible snapshot',
> id = 2,
> createTime = 2026-02-17T08:57:39.122511Z,
> state = 'poweredOff',
> quiesced = false,
> backupManifest = <unset>,
> childSnapshotList = (vim.vm.SnapshotTree) [
> (vim.vm.SnapshotTree) {
> dynamicType = <unset>,
> dynamicProperty = (vmodl.DynamicProperty) [],
> snapshot = 'vim.vm.Snapshot:1-snapshot-3',
> vm = 'vim.VirtualMachine:1',
> name = '虚拟机快照 2026%252f2%252f19 12:28:36', # 第二层快照
> description = '',
> id = 3,
> createTime = 2026-02-19T04:28:38.007703Z,
> state = 'poweredOn',
> quiesced = false,
> backupManifest = <unset>,
> childSnapshotList = (vim.vm.SnapshotTree) [],
> replaySupported = false
> }
> ],
> replaySupported = false
> }
> ],
> replaySupported = false
>}
>]
>}
## PY 文件作用
``` powershell
PS D:\PycharmProjects\RemoveWeeklySnapshot> tree /F
卷 Date 的文件夹 PATH 列表
卷序列号为 0E45-0F72
D:.
│ main.py
│ README.md
│ requirements.txt
├─config
│ │ config.yaml
│ │ settings.py
├─core
│ │ data_exporter.py
│ │ get_vm_snapshots.py
│ │ remove_snapshots.py
│ │ vm_connector.py
├─logs
│ 2026-02-21-vm_snapshot_cleanup.log
├─output
│ old_snapshots-2026-02-21.yaml
│ vm_snapshots_report-2026-02-21.xlsx
├─utils
│ │ logger.py
```
## 所用到的 Python 库
``` powershell
PS D:\PycharmProjects\RemoveWeeklyShapshot> pip freeze > requirements.txt
APScheduler==3.11.2
openpyxl==3.2.0b1
pandas==3.0.1
pyvmomi==9.0.0.0
PyYAML==6.0.3
```
``` shell
pip install -r requirements.txt
```
## 构建 Docker 镜像
### 新建数据目录
``` shell
mkdir -p ~/removeweeklysnapshot/{config,logs,output} && cd ~/removeweeklysnapshot
```
### 安装 Docker
``` shell
sudo curl https://download.docker.com/linux/centos/docker-ce.repo -o /etc/yum.repos.d/docker.repo
sudo dnf install docker-ce -y && docker -v
sudo systemctl enable --now docker && systemctl status docker
```
### Dockerfile 文件
``` dockerfile
cat << 'EOF' > Dockerfile
FROM python:3.14.3-slim
# 配置时区
ENV TZ=Asia/Shanghai
RUN ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && dpkg-reconfigure -f noninteractive tzdata
# 下载代码
ADD https://gitcode.junlan.site/junlan/RemoveWeeklyShapshot/archive/Dev.tar.gz /app/
# 解压压缩文件(最后 mv 命令改名)
RUN tar -xzf /app/Dev.tar.gz -C /app/ && rm /app/Dev.tar.gz && mv /app/removeweeklyshapshot /app/removeweeklysnapshot
# 设置工作目录
WORKDIR /app/removeweeklysnapshot
# 安装必要的软件和 python 库
RUN apt-get update && apt-get install procps tzdata -y && pip install -r requirements.txt && chmod +x main.py
# 添加项目根目录到 Python 路径
ENV PYTHONPATH=/app/removeweeklysnapshot
# 容器内执行启动程序
CMD ["python3", "/app/removeweeklysnapshot/main.py"]
EOF
```
### 执行构建 Docker 镜像
``` shell
sudo docker build --no-cache -t removeweeklysnapshot .
```
### 构建 Compose 文件
``` yaml
cat << 'EOF' > compose.yaml
services:
removeweeklysnapshot:
container_name: removeweeklysnapshot
image: removeweeklysnapshot
volumes:
- /home/junlan/removeweeklysnapshot/logs:/app/removeweeklysnapshot/logs
- /home/junlan/removeweeklysnapshot/output:/app/removeweeklysnapshot/output
# - /home/junlan/removeweeklysnapshot/config/config.yaml:/app/removeweeklysnapshot/config/config.yaml
restart: always
stdin_open: true
tty: true
EOF
```
### 运行容器
``` shell
sudo docker compose up -d
```
### 查看状态
``` shell
# 查看镜像构建历史记录
[junlan@localhost ~]$ sudo docker history removeweeklysnapshot
IMAGE CREATED CREATED BY SIZE COMMENT
ff84aa4fde3b 35 minutes ago CMD ["python3" "/app/removeweeklysnapshot/ma… 0B buildkit.dockerfile.v0
<missing> 35 minutes ago ENV PYTHONPATH=/app/removeweeklysnapshot 0B buildkit.dockerfile.v0
<missing> 35 minutes ago RUN /bin/sh -c apt-get update && apt-get ins… 216MB buildkit.dockerfile.v0
<missing> 36 minutes ago WORKDIR /app/removeweeklysnapshot 0B buildkit.dockerfile.v0
<missing> 36 minutes ago RUN /bin/sh -c tar -xzf /app/Dev.tar.gz -C /… 51.4kB buildkit.dockerfile.v0
<missing> 36 minutes ago ADD https://gitcode.junlan.site/junlan/Remov… 16.6kB buildkit.dockerfile.v0
<missing> 36 minutes ago RUN /bin/sh -c ln -fs /usr/share/zoneinfo/$T… 1.65MB buildkit.dockerfile.v0
<missing> 36 minutes ago ENV TZ=Asia/Shanghai 0B buildkit.dockerfile.v0
<missing> 2 weeks ago CMD ["python3"] 0B buildkit.dockerfile.v0
<missing> 2 weeks ago RUN /bin/sh -c set -eux; for src in idle3 p… 36B buildkit.dockerfile.v0
<missing> 2 weeks ago RUN /bin/sh -c set -eux; savedAptMark="$(a… 36.6MB buildkit.dockerfile.v0
<missing> 2 weeks ago ENV PYTHON_SHA256=a97d5549e9ad81fe17159ed02c… 0B buildkit.dockerfile.v0
<missing> 2 weeks ago ENV PYTHON_VERSION=3.14.3 0B buildkit.dockerfile.v0
<missing> 2 weeks ago RUN /bin/sh -c set -eux; apt-get update; a… 3.81MB buildkit.dockerfile.v0
<missing> 2 weeks ago ENV PATH=/usr/local/bin:/usr/local/sbin:/usr… 0B buildkit.dockerfile.v0
<missing> 2 weeks ago # debian.sh --arch 'amd64' out/ 'trixie' '@1… 78.6MB debuerreotype 0.17
[junlan@localhost removeweeklyshapshot]$ sudo docker images
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
python:3.14.3-slim 486b8092bfb1 176MB 45.5MB
removeweeklysnapshot:latest 6f17fcaaef99 512MB 140MB
[junlan@localhost removeweeklyshapshot]$ sudo docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
removeweeklysnapshot removeweeklysnapshot "python3 /removeweek…" removeweeklysnapshot 34 seconds ago Up 15 seconds
[junlan@localhost removeweeklyshapshot]$ sudo docker compose logs removeweeklysnapshot -f
removeweeklysnapshot | 2026-02-22 16:01:50,113 - INFO - ✅ 成功加载配置,共 1 个管理节点
removeweeklysnapshot | 2026-02-22 16:01:50,954 - INFO - ✓ 导出任务已设置: 每周 sun 16:05
removeweeklysnapshot | 2026-02-22 16:01:50,954 - INFO - ✓ 删除任务已设置: 每周 sun 16:10
removeweeklysnapshot | 2026-02-22 16:01:50,956 - INFO - 调度器已启动,等待执行任务...
removeweeklysnapshot | 2026-02-22 16:05:00,002 - INFO - 🔍 开始收集VM和快照信息...
removeweeklysnapshot | 2026-02-22 16:05:00,269 - INFO - 成功连接到节点: vcsa8.snimay.com
removeweeklysnapshot | 2026-02-22 16:05:01,251 - INFO - 获取到 5 台虚拟机
removeweeklysnapshot | 2026-02-22 16:05:01,252 - INFO - 📝 开始导出Excel报表...
removeweeklysnapshot | 2026-02-22 16:05:01,257 - INFO - 总共有 4 个快照
removeweeklysnapshot | 2026-02-22 16:05:01,377 - DEBUG - Excel 文件已生成: /app/removeweeklysnapshot/output/vm_snapshots_report-2026-02-22.xlsx
removeweeklysnapshot | 2026-02-22 16:05:01,377 - INFO - 📝 开始导出 Yaml 文件...
removeweeklysnapshot | 2026-02-22 16:05:01,378 - INFO - 可删除的快照有 0 个
removeweeklysnapshot | 2026-02-22 16:05:01,379 - DEBUG - YAML 文件已生成: /app/removeweeklysnapshot/output/old_snapshots-2026-02-22.yaml
removeweeklysnapshot | 2026-02-22 16:05:01,379 - INFO - ========== Excel和Yaml文件导出完成 ==========
[junlan@localhost removeweeklyshapshot]$ sudo docker exec -it removeweeklysnapshot bash
root@07c30da6408a:/removeweeklysnapshot# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 15:12 pts/0 00:00:01 python3 /removeweeklysnapshot/main.py
root 11 0 0 15:15 pts/1 00:00:00 bash
root 17 11 0 15:15 pts/1 00:00:00 ps -ef
```
### 修改代码进行临时测试
- 修改 ==config.yaml== 文件
- `snapshot_retention_days: 0` :定义删除多少天前的快照
- `schedule`:自定义改参数下的时间
``` yaml
cat << 'EOF' > ~/removeweeklysnapshot/config/config.yaml
# 管理节点配置包含vCenter和ESXi
management_nodes:
# vCenter节点
- type: vcenter # 标记类型为vcenter
name: vc1 # 节点名称(用于日志)
host: vcsa8.xxx.com # 地址
user: administrator@vcsa.local
password: Root@2020
max_delete_concurrent: 4 # 该节点最大并发删除数
# 全局策略配置
global:
disable_ssl_verify: true # ESXi连接特殊配置禁用SSL验证ESXi默认自签证书
snapshot_retention_days: 1 # 可选,默认值 15 天
# 定时任务配置
schedule:
export: # 导出 Excel 和 Yaml 文件的时间
day_of_week: 'sun' # 星期几mon,tue,wed,thu,fri,sat,sun
hour: 15 # 小时 (0-23)
minute: 58 # 分钟 (0-59)
second: 0 # 秒 (可选)
delete: # 删除快照的时间
day_of_week: 'sun'
hour: 15
minute: 59
second: 0
EOF
==================================================================================================
cat << 'EOF' > compose.yaml
services:
removeweeklysnapshot:
container_name: removeweeklysnapshot
image: removeweeklysnapshot
volumes:
- ~/removeweeklysnapshot/logs:/app/removeweeklysnapshot/logs
- ~/removeweeklysnapshot/output:/app/removeweeklysnapshot/output
- ~/removeweeklysnapshot/config/config.yaml:/app/removeweeklysnapshot/config/config.yaml
restart: always
stdin_open: true
tty: true
EOF
```
#### 进入容器测试
手动执行 `main.py` 运行
``` shell
[junlan@localhost removeweeklysnapshot]$ sudo docker exec -it removeweeklysnapshot bash
root@63869672d333:/app/removeweeklyshapshot# python main.py
2026-02-21 15:42:56,070 - INFO - ✅ 成功加载配置,共 2 个管理节点
2026-02-21 15:42:56,556 - INFO - 定时任务已设置每周六凌晨4点导出文件晚上7点删除快照
2026-02-21 15:43:00,000 - INFO - 🔍 开始收集VM和快照信息...
2026-02-21 15:43:00,054 - INFO - 成功连接到节点: 192.168.40.133
2026-02-21 15:43:03,319 - ERROR - 处理节点 192.168.40.135 失败:[Errno 113] No route to host
2026-02-21 15:43:03,319 - INFO - 获取到 2 台虚拟机
2026-02-21 15:43:03,319 - INFO - 📝 开始导出Excel报表...
2026-02-21 15:43:03,322 - INFO - 总共有 3 个快照
2026-02-21 15:43:03,340 - DEBUG - Excel 文件已生成: /removeweeklysnapshot/output/vm_snapshots_report.xlsx
2026-02-21 15:43:03,340 - INFO - 📝 开始导出 Yaml 文件...
2026-02-21 15:43:03,340 - INFO - 可删除的快照有 3 个
2026-02-21 15:43:03,342 - DEBUG - YAML 文件已生成: /removeweeklysnapshot/output/old_snapshots.yaml
2026-02-21 15:43:03,342 - INFO - ========== Excel和Yaml文件导出完成 ==========
2026-02-21 15:44:00,001 - INFO - 🗑️ 开始删除过旧快照...
2026-02-21 15:44:00,004 - INFO - 连接 192.168.40.133 进行删除快照...
2026-02-21 15:44:00,050 - INFO - 成功连接到节点: 192.168.40.133
2026-02-21 15:44:00,081 - INFO - 正在删除 Snapshot: VMware vCenter Server Appliance-快照测试-(1-snapshot-7)
2026-02-21 15:44:01,100 - INFO - ✅ 删除成功: VMware vCenter Server Appliance-快照测试-(1-snapshot-7)
2026-02-21 15:44:01,125 - INFO - 正在删除 Snapshot: VMware vCenter Server Appliance-快照测试-第二层快照-(1-snapshot-8)
2026-02-21 15:44:02,139 - INFO - ✅ 删除成功: VMware vCenter Server Appliance-快照测试-第二层快照-(1-snapshot-8)
2026-02-21 15:44:02,164 - INFO - 正在删除 Snapshot: test-vm-01-snap-01-(4-snapshot-9)
2026-02-21 15:44:03,178 - INFO - ✅ 删除成功: test-vm-01-snap-01-(4-snapshot-9)
2026-02-21 15:44:03,184 - INFO - ========== VM快照清理任务执行完成 ==========
```

45
config/config.yaml Normal file
View File

@@ -0,0 +1,45 @@
# 管理节点配置包含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性能较弱并发数可设小些
- type: esxi
name: esxi2
host: 192.168.40.135
user: root
password: Root@2025
max_delete_concurrent: 2
# 全局策略配置
global:
disable_ssl_verify: true # ESXi连接特殊配置禁用SSL验证ESXi默认自签证书
snapshot_retention_days: 15 # 可选,默认值 15 天
# excel_output_path: ./vm_snapshots_report.xlsx # 可选,使用默认值,如:/logs/2026-02-20_14-00-21-VMsSnapShots_report.xlsx
# yaml_output_path: ./yaml_snapshots_report.yaml # 可选
# 定时任务配置
schedule:
export: # 导出 Excel 和 Yaml 文件的时间
day_of_week: 'sat' # 星期几mon,tue,wed,thu,fri,sat,sun
hour: 6 # 小时 (0-23)
minute: 0 # 分钟 (0-59)
second: 0 # 秒 (可选)
delete: # 删除快照的时间
day_of_week: 'sat'
hour: 19
minute: 0
second: 0

View File

@@ -1,33 +1,84 @@
import os
from datetime import datetime, timedelta
from dotenv import load_dotenv
import os, yaml
from datetime import datetime
from utils.logger import logger
# 加载.env文件
load_dotenv()
# ========== 基础配置 ==========
# vCenter配置
VCENTER_HOSTS = os.getenv('VCENTER_HOSTS', '').split(',')
VCENTER_USER = os.getenv('VCENTER_USER', '')
VCENTER_PASSWORD = os.getenv('VCENTER_PASSWORD', '')
def load_config():
"""加载 YAML 配置文件并解析其内容"""
try:
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
raw_config = yaml.safe_load(f)
# 快照策略配置
SNAPSHOT_RETENTION_DAYS = int(os.getenv('SNAPSHOT_RETENTION_DAYS', 15))
MAX_DELETE_CONCURRENT = int(os.getenv('MAX_DELETE_CONCURRENT', 4))
# 全局配置
global_config = raw_config.get('global', {})
# 输出路径配置
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')
# 读取定时任务配置(带默认值)
schedule_config = global_config.get('schedule', {})
export_schedule = schedule_config.get('export', {'day_of_week': 'sat','hour': 4,'minute': 0, 'second': 0})
delete_schedule = schedule_config.get('delete', {'day_of_week': 'sat', 'hour': 19, 'minute': 0, 'second': 0})
# 计算快照过期时间(全局变量)
EXPIRE_DATE = datetime.now() - timedelta(days=SNAPSHOT_RETENTION_DAYS)
config = {
# vCenter/ESXi节点列表
"MANAGEMENT_NODES": raw_config.get('management_nodes', []),
"SNAPSHOT_RETENTION_DAYS": int(global_config.get('snapshot_retention_days', 15)),
"EXCEL_OUTPUT_PATH": global_config.get('excel_output_path', os.path.join(DATA_DIR, f'vm_snapshots_report-{datetime.now().strftime('%Y-%m-%d')}.xlsx')),
"YAML_OUTPUT_PATH": global_config.get('yaml_output_path', os.path.join(DATA_DIR, f'old_snapshots-{datetime.now().strftime('%Y-%m-%d')}.yaml')),
"DISABLE_SSL_VERIFY": global_config.get('disable_ssl_verify', True),
"SCHEDULE_EXPORT": export_schedule,
"SCHEDULE_DELETE": delete_schedule,
}
# 验证必要配置
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信息和策略配置")
# 验证配置
if not config["MANAGEMENT_NODES"]:
raise ValueError("未配置任何管理节点vCenter 或 ESXi至少要有一台 management_nodes 节点。")
# 检查每个节点的必填字段
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
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 获取项目根目录
DATA_DIR = os.path.join(BASE_DIR, 'output') # 获取导出数据的文件根目录
os.makedirs(DATA_DIR, exist_ok=True) # 自动创建目录
# 配置文件路径
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config.yaml')
# 加载配置并导出全局变量
config = load_config() # 模块导入时立即执行
MANAGEMENT_NODES = config["MANAGEMENT_NODES"]
SNAPSHOT_RETENTION_DAYS = config["SNAPSHOT_RETENTION_DAYS"]
EXCEL_OUTPUT_PATH = config["EXCEL_OUTPUT_PATH"]
YAML_OUTPUT_PATH = config["YAML_OUTPUT_PATH"]
DISABLE_SSL_VERIFY = config["DISABLE_SSL_VERIFY"]
# 调度配置
SCHEDULE_EXPORT = config["SCHEDULE_EXPORT"]
SCHEDULE_DELETE = config["SCHEDULE_DELETE"]
if __name__ == "__main__":
# 打印全局配置
print("\n【全局配置】")
print(f" 快照保留天数: {config['SNAPSHOT_RETENTION_DAYS']}")
print(f" Excel输出路径: {EXCEL_OUTPUT_PATH}")
# 打印管理节点
nodes = config['MANAGEMENT_NODES']
print(f"\n【管理节点】共 {len(nodes)}")
for i, node in enumerate(nodes, 1):
print(f"\n 节点[{i}]:")
print(f" 地址: {node.get('host')}")
print(f" 用户: {node.get('user')}")
print(f" 密码: {'*' * len(node.get('password', ''))}")
# print(f" 密码: {node.get('password', '')}") # 直接打印出密码

130
core/data_exporter.py Normal file
View File

@@ -0,0 +1,130 @@
import yaml
import pandas as pd
from datetime import datetime, timedelta
from openpyxl.styles import Border, Side, Font, PatternFill
from utils.logger import logger
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 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)
"""设置表格样式"""
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')
# 输出待删除的旧快照到 YAML 文件
def export_yaml(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() # 导出 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)

127
core/remove_snapshots.py Normal file
View File

@@ -0,0 +1,127 @@
import os, time, yaml
from pyVmomi import vim
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 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 remove_snapshot(dele_snapshots):
"""根据 YAML 信息删除旧快照"""
# print(dele_snapshots)
if not dele_snapshots:
logger.info("没有需要删除的快照")
return
# 按 NodeHost 分组,避免重复连接
grouped = {}
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} 进行删除快照...")
try:
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']) # 根据快照名,查找出相应的虚拟机
# print(snap_name,vm)
if not vm:
logger.info(f"未找到 VM: {snapshot_info['VMName']}")
return
# 检查该虚拟机是否有快照
if not vm.snapshot:
logger.warning(f"{snap_name}:快照不存在")
return
# 调用函数获取虚拟机快照的 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
try:
"""删除快照核心代码,调用快照对象的 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) # 每秒检查一次
# 更完整的状态判断
if task.info.state == vim.TaskInfo.State.success:
logger.info(f"✅ 删除成功: {snap_name}")
elif task.info.state == vim.TaskInfo.State.error:
error_msg = task.info.error.msg if task.info.error else "未知错误"
logger.error(f"❌ 删除失败: {snap_name}, 错误: {error_msg}")
else:
# 处理其他状态(如 cancelled
logger.warning(f"⚠️ 任务未成功完成,状态: {task.info.state}")
except Exception as e:
logger.exception(f"删除快照时发生异常: {snap_name}")
raise
"""根据 VM 名称查找虚拟机"""
def find_vm_by_name(content, vm_name):
container = content.viewManager.CreateContainerView(
content.rootFolder, [vim.VirtualMachine], True
)
for vm in container.view:
if vm.name == vm_name:
container.Destroy()
return vm
container.Destroy()
return None
""" 递归查找 snapshot 对象 """
def find_snapshot_by_moid(snapshot_tree, moid):
for snapshot in snapshot_tree:
if snapshot.snapshot._moId == moid:
return snapshot.snapshot
if snapshot.childSnapshotList:
result = find_snapshot_by_moid(snapshot.childSnapshotList, moid)
if result:
return result
return None
if __name__ == "__main__":
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)

86
main.py
View File

@@ -1,20 +1,82 @@
# 这是一个示例 Python 脚本。
# 按 Shift+F10 执行或将其替换为您的代码。
# 按 双击 Shift 在所有地方搜索类、文件、工具窗口、操作和设置。
import time
from apscheduler.schedulers.background import BackgroundScheduler
from utils.logger import logger
from config.settings import YAML_OUTPUT_PATH,SCHEDULE_EXPORT, SCHEDULE_DELETE
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():
"""主执行函数"""
export_conf = SCHEDULE_EXPORT # 从配置读取导出任务时间
delete_conf = SCHEDULE_DELETE # 从配置读取删除任务时间
scheduler = BackgroundScheduler() # 创建调度器
# 1. 添加导出数据任务
scheduler.add_job(export_files, # 要执行的函数
'cron', # 触发器类型
day_of_week=export_conf['day_of_week'],
hour=export_conf['hour'],
minute=export_conf['minute'],
second=export_conf.get('second', 0),
id='export_files', # 任务唯一ID
name='导出Excel和YAML' # 任务名称
)
logger.info(f"✓ 导出任务已设置: 每周 {export_conf['day_of_week']} {export_conf['hour']:02d}:{export_conf['minute']:02d}")
# 2. 添加删除任务
scheduler.add_job(
delete_old_snapshots, # 要执行的函数
'cron',
day_of_week=delete_conf['day_of_week'],
hour=delete_conf['hour'],
minute=delete_conf['minute'],
second=delete_conf.get('second', 0),
id='delete_snapshots',
name='删除旧快照'
)
logger.info(f"✓ 删除任务已设置: 每周 {delete_conf['day_of_week']} {delete_conf['hour']:02d}:{delete_conf['minute']:02d}")
# 启动调度器
scheduler.start()
logger.info("调度器已启动,等待执行任务...")
try:
# 保持主程序运行
while True:
time.sleep(1)
except (KeyboardInterrupt, SystemExit):
logger.info("正在关闭调度器...")
scheduler.shutdown()
logger.info("调度器已关闭")
if __name__ == "__main__":
main()

BIN
requirements.txt Normal file

Binary file not shown.

39
utils/logger.py Normal file
View File

@@ -0,0 +1,39 @@
import os, logging
from datetime import datetime
def get_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')
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 获取项目根目录
LOG_DIR = os.path.join(BASE_DIR, 'logs') # 获取日志文件根目录
os.makedirs(LOG_DIR, exist_ok=True) # 自动创建目录
log_file = os.path.join(LOG_DIR, f'{datetime.now().strftime("%Y-%m-%d")}-removeweeklysnapshot.log') # 日志文件路径
# 文件处理器(写入日志文件)
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setFormatter(formatter) # 应用格式化器
# 控制台处理器(输出到终端)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
# 添加处理器
logger.addHandler(file_handler)
logger.addHandler(console_handler)
logger.setLevel(logging.DEBUG) # 默认只会记录Info以上级别的日志
return logger
# 全局logger实例
logger = get_logger()

View File

@@ -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连接")