Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af0411783 | ||
|
|
e8ed320193 | ||
|
|
375a54187d | ||
|
|
e505148026 | ||
|
|
019c009700 | ||
|
|
26678accb7 | ||
|
|
d462263b39 | ||
|
|
7c95ccb2af | ||
|
|
17e9e0c3bc | ||
|
|
9024e9c8e4 | ||
|
|
7a0a51be4e | ||
|
|
199cbab4aa | ||
|
|
3332e3b865 |
14
.env
14
.env
@@ -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
620
README.md
@@ -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
45
config/config.yaml
Normal 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
|
||||
@@ -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
130
core/data_exporter.py
Normal 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
100
core/get_vm_snapshots.py
Normal 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
127
core/remove_snapshots.py
Normal 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
36
core/vm_connector.py
Normal 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
86
main.py
@@ -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
BIN
requirements.txt
Normal file
Binary file not shown.
39
utils/logger.py
Normal file
39
utils/logger.py
Normal 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()
|
||||
@@ -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连接")
|
||||
Reference in New Issue
Block a user