vmware的python自动化:批量克隆虚拟机
·
目录
自动化需求
根据xlsx表格信息批量克隆虚拟机(支持多硬盘,支持IP信息设置,支持自定义规范设置)
版本:
python3.11
pyvmomi 9.0.0.0
pyvim 3.0.3
代码
import ssl
import os
from collections import defaultdict
from pyVim.connect import SmartConnect, Disconnect
from pyVmomi import vim
from pyVim.task import WaitForTask
from openpyxl import load_workbook
# ================= 配置区域:在此处添加多个vCenter信息 =================
VC_CONFIGS = {
'192.168.1.250': {
'user': 'administrator@vsphere.local',
'pwd': 'xiaozhou@666.com'
},
# 示例:添加第二个vCenter
# '192.168.1.100': {
# 'user': 'administrator@vsphere.local',
# 'pwd': 'YourPasswordHere'
# }
}
def get_obj(content, vimtype, name=None):
"""获取对象函数"""
container = content.viewManager.CreateContainerView(content.rootFolder, vimtype, True)
return next((item for item in container.view if item.name == name), None) if name else container.view
def read_vm_config_from_xlsx():
"""
从当前路径下的vm.xlsx读取虚拟机配置信息
Excel格式:VC,主机名称,存储名称,文件夹名称,模板名称,自定义规范名称,虚拟机名称,主机名,CPU数量,内存大小,端口组,IP地址,子网掩码,网关,DNS1,DNS2,硬盘,备注
【新增】硬盘列:支持多个硬盘,换行分隔,单位G(例如:100\n200\n50)
"""
vm_configs = []
xlsx_file_path = os.path.join(os.getcwd(), 'vm.xlsx')
if not os.path.exists(xlsx_file_path):
print(f"错误: Excel文件不存在 '{xlsx_file_path}'")
return vm_configs
try:
wb = load_workbook(xlsx_file_path, read_only=True, data_only=True)
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
wb.close()
if not rows:
print("错误: Excel文件为空")
return vm_configs
header = [str(h).strip() if h else '' for h in rows[0]]
# 【修改】验证Excel格式,新增「硬盘」列
expected_headers = ['VC', '主机名称', '存储名称', '文件夹名称', '模板名称', '自定义规范名称',
'虚拟机名称', '主机名', 'CPU数量', '内存大小', '端口组',
'IP地址', '子网掩码', '网关', 'DNS1', 'DNS2', '硬盘', '备注']
if header != expected_headers:
print(f"警告: Excel表头格式不正确")
print(f"期望: {expected_headers}")
print(f"实际: {header}")
return vm_configs
for row in rows[1:]:
if len(row) >= 18:
# 【修改】读取硬盘列,支持换行分隔的多个硬盘
disk_sizes_str = str(row[16]).strip() if row[16] else ''
disk_sizes = []
if disk_sizes_str:
# 按换行分割多个硬盘,过滤空行,转换为整数
for size_str in disk_sizes_str.split('\n'):
size_str = size_str.strip()
if size_str and size_str.isdigit():
disk_sizes.append(int(size_str))
config = {
'vc_ip': str(row[0]).strip() if row[0] else '',
'host_name': str(row[1]).strip() if row[1] else '',
'datastore_name': str(row[2]).strip() if row[2] else '',
'folder_name': str(row[3]).strip() if row[3] else '',
'template_name': str(row[4]).strip() if row[4] else '',
'custom_spec_name': str(row[5]).strip() if row[5] else None,
'vm_name': str(row[6]).strip() if row[6] else '',
'hostname': str(row[7]).strip() if row[7] else '',
'cpu_num': int(row[8]) if row[8] and str(row[8]).strip() else 2,
'mem_num': int(row[9]) if row[9] and str(row[9]).strip() else 4,
'port_group': str(row[10]).strip() if row[10] else '',
'nic_ip': str(row[11]).strip() if row[11] else None,
'nic_netmask': str(row[12]).strip() if row[12] else '255.255.255.0',
'nic_gateway': str(row[13]).strip() if row[13] else None,
'dns1': str(row[14]).strip() if row[14] and str(row[14]).strip() else '',
'dns2': str(row[15]).strip() if row[15] and str(row[15]).strip() else '',
'disk_sizes': disk_sizes, # 【新增】硬盘大小列表,单位G
'remark': str(row[17]).strip() if row[17] else ''
}
# 验证VC IP是否在配置中
if config['vc_ip'] not in VC_CONFIGS:
print(f"警告: 跳过虚拟机 '{config['vm_name']}',VC '{config['vc_ip']}' 未在代码中配置")
continue
if config['vm_name'] and config['hostname'] and config['vc_ip']:
vm_configs.append(config)
dns_status = f"DNS: {config['dns1']},{config['dns2']}" if config['dns1'] or config['dns2'] else "DNS: 自动获取"
disk_status = f"硬盘: {config['disk_sizes']}G" if config['disk_sizes'] else "硬盘: 不新增"
print(
f"读取配置: {config['vm_name']} - VC: {config['vc_ip']} - 模板: {config['template_name']} - {disk_status} - {dns_status} - 备注: {config['remark'] or '无'}")
else:
print(f"警告: 跳过无效行(缺少VC/虚拟机名称/主机名): {row}")
else:
print(f"警告: Excel行数据不完整: {row}")
except Exception as e:
print(f"读取Excel文件失败: {e}")
return vm_configs
def parse_dns_servers(dns1, dns2):
"""
解析DNS服务器
- 填写了DNS则返回有效地址列表
- 未填写则返回空列表,代表不设置固定DNS,自动获取
"""
dns_servers = []
if dns1 and dns1.strip():
dns_servers.append(dns1.strip())
if dns2 and dns2.strip():
dns_servers.append(dns2.strip())
return dns_servers
def get_customization_spec(content, spec_name):
"""从vCenter获取自定义规范"""
spec_manager = content.customizationSpecManager
try:
spec_item = spec_manager.GetCustomizationSpec(name=spec_name)
return spec_item.spec
except vim.fault.NotFound:
print(f"错误: 找不到自定义规范 '{spec_name}'")
return None
except Exception as e:
print(f"获取自定义规范 '{spec_name}' 失败: {e}")
return None
def power_on_vm(vm, vm_name):
"""虚拟机开机通用函数,带状态校验和异常处理"""
try:
# 校验虚拟机电源状态
power_state = vm.runtime.powerState
if power_state == vim.VirtualMachinePowerState.poweredOn:
print(f'虚拟机 {vm_name} 已处于开机状态,跳过开机操作')
return True
if power_state == vim.VirtualMachinePowerState.suspended:
print(f'警告: 虚拟机 {vm_name} 处于挂起状态,无法执行开机')
return False
# 执行开机任务
print(f'正在启动虚拟机: {vm_name}')
power_on_task = vm.PowerOnVM_Task()
WaitForTask(power_on_task)
print(f'✅ 虚拟机 {vm_name} 开机成功')
return True
except Exception as e:
print(f'❌ 虚拟机 {vm_name} 开机失败: {str(e)}')
return False
def process_vcenter_tasks(vc_ip, vc_cred, vm_list):
"""处理单个vCenter下的所有克隆任务"""
print(f"\n========== 开始处理 vCenter: {vc_ip} ==========")
si = None
context = ssl._create_unverified_context()
try:
# 连接vCenter
si = SmartConnect(
host=vc_ip,
user=vc_cred['user'],
pwd=vc_cred['pwd'],
port=443,
sslContext=context
)
content = si.content
print(f"成功连接到 vCenter: {vc_ip}")
except Exception as e:
print(f"连接vCenter {vc_ip} 失败: {e}")
return
try:
# 获取该VC下的必要对象
networks = get_obj(content, [vim.Network], None)
all_hosts = get_obj(content, [vim.HostSystem], None)
total_num = len(vm_list)
task_list = []
for index, vm_config in enumerate(vm_list, start=1):
host_name = vm_config['host_name']
datastore_name = vm_config['datastore_name']
folder_name = vm_config['folder_name']
template_name = vm_config['template_name']
custom_spec_name = vm_config['custom_spec_name']
vm_name = vm_config['vm_name']
hostname = vm_config['hostname']
cpu_num = vm_config['cpu_num']
mem_num = vm_config['mem_num']
port_group = vm_config['port_group']
nic_ip = vm_config['nic_ip']
nic_netmask = vm_config['nic_netmask']
nic_gateway = vm_config['nic_gateway']
dns1 = vm_config['dns1']
dns2 = vm_config['dns2']
disk_sizes = vm_config['disk_sizes'] # 【新增】硬盘大小列表
vm_remark = vm_config['remark']
# 获取文件夹
folder = get_obj(content, [vim.Folder], folder_name)
if not folder:
print(f"错误: 找不到文件夹 '{folder_name}',跳过虚拟机 '{vm_name}'")
continue
# 获取模板虚拟机
template_vm = get_obj(content, [vim.VirtualMachine], template_name)
if not template_vm:
print(f"错误: 找不到模板虚拟机 '{template_name}',跳过虚拟机 '{vm_name}'")
continue
# 获取目标主机
host = next((h for h in all_hosts if h.name == host_name), None)
if not host:
print(f"错误: 找不到主机 '{host_name}',跳过虚拟机 '{vm_name}'")
continue
# 获取存储
datastore = next((ds for ds in host.datastore if ds.name == datastore_name), None)
if not datastore:
print(f"错误: 在主机 '{host_name}' 上找不到存储 '{datastore_name}',跳过虚拟机 '{vm_name}'")
print(f"可用存储: {[ds.name for ds in host.datastore]}")
continue
# 获取资源池
if isinstance(host.parent, vim.ClusterComputeResource):
pool = host.parent.resourcePool
else:
pool = host.resourcePool
if not pool:
print(f"错误: 无法获取资源池,跳过虚拟机 '{vm_name}'")
continue
# 准备虚拟机配置
vmconf = vim.vm.ConfigSpec()
vmconf.numCPUs = cpu_num
vmconf.memoryMB = mem_num * 1024
vmconf.guestId = template_vm.config.guestId
vmconf.annotation = vm_remark # 写入虚拟机备注
vmconf.deviceChange = []
# ================= 1. 删除模板原有网卡 =================
template_nics = [dev for dev in template_vm.config.hardware.device if
isinstance(dev, vim.vm.device.VirtualEthernetCard)]
for nic in template_nics:
vmconf.deviceChange.append(vim.vm.device.VirtualDeviceSpec(
device=nic,
operation=vim.vm.device.VirtualDeviceSpec.Operation.remove
))
# ================= 2. 配置新网络 =================
network = next((n for n in networks if n.name == port_group), None)
if not network:
print(f"警告: 端口组 '{port_group}' 不存在,跳过虚拟机 '{vm_name}'")
continue
# 创建网卡Backing
if hasattr(network, 'config') and hasattr(network.config, 'distributedVirtualSwitch'):
port_conn = vim.dvs.PortConnection(
portgroupKey=network.key,
switchUuid=network.config.distributedVirtualSwitch.uuid
)
backing = vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo(port=port_conn)
else:
backing = vim.vm.device.VirtualEthernetCard.NetworkBackingInfo(
deviceName=network.name,
network=network
)
# 创建新网卡
new_nic = vim.vm.device.VirtualVmxnet3()
new_nic.backing = backing
new_nic.key = -100
new_nic.addressType = 'generated'
new_nic.wakeOnLanEnabled = True
new_nic.connectable = vim.vm.device.VirtualDevice.ConnectInfo(
startConnected=True,
allowGuestControl=True,
connected=True
)
vmconf.deviceChange.append(vim.vm.device.VirtualDeviceSpec(
device=new_nic,
operation=vim.vm.device.VirtualDeviceSpec.Operation.add
))
# ================= 【新增】3. 配置新增硬盘(固定精简制备) =================
if disk_sizes:
# 获取模板的现有硬盘和控制器
template_disks = [dev for dev in template_vm.config.hardware.device if
isinstance(dev, vim.vm.device.VirtualDisk)]
template_controllers = [dev for dev in template_vm.config.hardware.device if
isinstance(dev, (vim.vm.device.VirtualSCSIController,
vim.vm.device.ParaVirtualSCSIController,
vim.vm.device.VirtualLsiLogicController,
vim.vm.device.VirtualLsiLogicSASController))]
if not template_controllers:
print(f"警告: 模板 '{template_name}' 没有SCSI控制器,无法新增硬盘,跳过虚拟机 '{vm_name}'")
continue
# 使用第一个SCSI控制器
scsi_controller = template_controllers[0]
# 找到最大的unitNumber,用于新硬盘递增
used_unit_numbers = set()
for disk in template_disks:
if disk.controllerKey == scsi_controller.key:
used_unit_numbers.add(disk.unitNumber)
# SCSI控制器的unitNumber 7通常留给控制器本身
next_unit_number = 1
while next_unit_number in used_unit_numbers or next_unit_number == 7:
next_unit_number += 1
# 为每个新增硬盘创建配置
for disk_size_gb in disk_sizes:
if disk_size_gb <= 0:
continue
# 创建新硬盘
new_disk = vim.vm.device.VirtualDisk()
new_disk.key = -200 - len(vmconf.deviceChange) # 唯一key
new_disk.controllerKey = scsi_controller.key
new_disk.unitNumber = next_unit_number
new_disk.capacityInKB = disk_size_gb * 1024 * 1024 # GB转KB
# 【核心】配置精简制备
disk_backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
disk_backing.fileName = "" # 留空,由vCenter自动生成
disk_backing.datastore = datastore
disk_backing.diskMode = "persistent"
disk_backing.thinProvisioned = True # 强制精简制备
new_disk.backing = disk_backing
# 添加到设备变更列表
disk_spec = vim.vm.device.VirtualDeviceSpec()
disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
disk_spec.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.create # 创建新文件
disk_spec.device = new_disk
vmconf.deviceChange.append(disk_spec)
print(f" 配置虚拟机 {vm_name} 新增硬盘: {disk_size_gb}G (精简制备)")
# 递增下一个unitNumber
next_unit_number += 1
while next_unit_number in used_unit_numbers or next_unit_number == 7:
next_unit_number += 1
# ================= 4. 准备IP设置 =================
adapter_map = vim.vm.customization.AdapterMapping()
ip_settings = vim.vm.customization.IPSettings()
if nic_ip:
ip_settings.ip = vim.vm.customization.FixedIp(ipAddress=nic_ip)
ip_settings.subnetMask = nic_netmask
if nic_gateway:
ip_settings.gateway = [nic_gateway]
else:
ip_settings.ip = vim.vm.customization.DhcpIpGenerator()
adapter_map.adapter = ip_settings
adaptermaps = [adapter_map]
# ================= 5. 解析DNS =================
dns_server_list = parse_dns_servers(dns1, dns2)
if dns_server_list:
print(f"配置虚拟机 {vm_name} 固定DNS: {dns_server_list}")
else:
print(f"配置虚拟机 {vm_name} DNS: 自动获取(不设置固定DNS)")
# ================= 6. 处理自定义规范 =================
customization_spec = None
if custom_spec_name:
base_spec = get_customization_spec(content, custom_spec_name)
if not base_spec:
continue
# 复制并修改规范
customization_spec = vim.vm.customization.Specification()
customization_spec.identity = base_spec.identity
customization_spec.globalIPSettings = base_spec.globalIPSettings or vim.vm.customization.GlobalIPSettings()
customization_spec.nicSettingMap = base_spec.nicSettingMap or []
customization_spec.encryptionKey = base_spec.encryptionKey
# 修改主机名(固定覆盖)
if isinstance(customization_spec.identity, vim.vm.customization.Sysprep):
if not customization_spec.identity.userData:
customization_spec.identity.userData = vim.vm.customization.UserData()
customization_spec.identity.userData.computerName = vim.vm.customization.FixedName(name=hostname)
elif isinstance(customization_spec.identity, vim.vm.customization.LinuxPrep):
customization_spec.identity.hostName = vim.vm.customization.FixedName(name=hostname)
# 仅当填写了DNS时才覆盖,空值不修改原自定义规范的DNS配置
if dns_server_list:
customization_spec.globalIPSettings.dnsServerList = dns_server_list
# 修改网卡设置(替换第一个网卡)
if customization_spec.nicSettingMap:
customization_spec.nicSettingMap[0] = adapter_map
else:
customization_spec.nicSettingMap = adaptermaps
else:
# 无自定义规范时创建默认规范
guest_id = template_vm.config.guestId
is_windows = 'windows' in guest_id.lower()
if is_windows:
ident = vim.vm.customization.Sysprep()
ident.userData = vim.vm.customization.UserData()
ident.userData.computerName = vim.vm.customization.FixedName(name=hostname)
ident.userData.fullName = "Administrator"
ident.userData.orgName = "Organization"
ident.guiUnattended = vim.vm.customization.GuiUnattended()
ident.guiUnattended.timeZone = 85
ident.identification = vim.vm.customization.Identification()
ident.identification.joinWorkgroup = "WORKGROUP"
else:
ident = vim.vm.customization.LinuxPrep(
hostName=vim.vm.customization.FixedName(name=hostname),
domain="localdomain"
)
# 仅当填写了DNS时才设置固定DNS,空值不设置,自动获取
global_ip = vim.vm.customization.GlobalIPSettings()
if dns_server_list:
global_ip.dnsServerList = dns_server_list
customization_spec = vim.vm.customization.Specification(
identity=ident,
globalIPSettings=global_ip,
nicSettingMap=adaptermaps
)
# ================= 7. 构建克隆规范(保持powerOn=False,手动控制开机时机) =================
clonespec = vim.vm.CloneSpec(
powerOn=False,
template=False,
location=vim.vm.RelocateSpec(datastore=datastore, pool=pool, host=host),
config=vmconf,
customization=customization_spec
)
# 执行克隆
try:
task = template_vm.Clone(folder=folder, name=vm_name, spec=clonespec)
task_list.append((task, vm_name))
print(f"克隆任务已提交: {vm_name} ({vc_ip})")
# 批量任务处理(每5个任务执行一次等待+开机)
if len(task_list) >= 5:
for t, name in task_list:
try:
WaitForTask(t)
print(f'✅ 克隆任务完成: {name}')
# 获取新创建的虚拟机对象
new_vm = t.info.result
if not new_vm or not isinstance(new_vm, vim.VirtualMachine):
print(f'警告: 无法获取虚拟机 {name} 对象,跳过开机')
continue
# 执行开机
power_on_vm(new_vm, name)
except Exception as e:
print(f'❌ 操作失败 {name}: {e}')
task_list = []
except Exception as e:
print(f"克隆失败 '{vm_name}': {e}")
continue
print(f'进度: {index}/{total_num} (VC: {vc_ip}) - {vm_name}')
# 处理剩余的克隆任务+开机
for t, name in task_list:
try:
WaitForTask(t)
print(f'✅ 克隆任务完成: {name}')
new_vm = t.info.result
if not new_vm or not isinstance(new_vm, vim.VirtualMachine):
print(f'警告: 无法获取虚拟机 {name} 对象,跳过开机')
continue
power_on_vm(new_vm, name)
except Exception as e:
print(f'❌ 操作失败 {name}: {e}')
print(f"========== vCenter {vc_ip} 处理完成 ==========\n")
except Exception as e:
print(f"vCenter {vc_ip} 执行错误: {e}")
finally:
if si:
Disconnect(si)
print(f"已断开 vCenter {vc_ip} 连接")
def main():
# 读取所有配置
all_configs = read_vm_config_from_xlsx()
if not all_configs:
print("没有有效的虚拟机配置数据")
return
# 按VC IP分组
vc_groups = defaultdict(list)
for config in all_configs:
vc_groups[config['vc_ip']].append(config)
print(f"\n共读取到 {len(all_configs)} 个配置,分布在 {len(vc_groups)} 个vCenter上")
# 逐个处理每个vCenter
for vc_ip, vm_list in vc_groups.items():
if vc_ip in VC_CONFIGS:
process_vcenter_tasks(vc_ip, VC_CONFIGS[vc_ip], vm_list)
else:
print(f"警告: 跳过未配置的vCenter: {vc_ip}")
print("所有vCenter任务处理完毕")
if __name__ == "__main__":
main()
使用方法
在代码内填入vc的有关信息(支持多VC),在代码文件同级目录创建vm.xlsx,xlsx包括VC,主机名称,存储名称,文件夹名称,模板名称,自定义规范名称,虚拟机名称,主机名,CPU数量,内存大小,端口组,IP地址,子网掩码,网关,DNS1,DNS2,硬盘,备注等列;其中IP信息为空(IP地址,子网掩码,网关,DNS1,DNS2)则为DHCP获取;自定义规范名称为空则不应用自定义规范;硬盘为空则不添加额外硬盘,如需添加多个硬盘则换行给硬盘大小,单位为G(新增硬盘格式固定为精简制备);备注信息为空则不添加备注,克隆完成后会自动开机
vm.xlsx示意图如下:

脚本为ai编写,使用前请使用测试环境进行测试,此脚本对于Vcenter版本6.7及以下兼容性不佳,建议版本为Vcenter7.0及以上,具体表现为linux虚拟机无法写入IP地址,无法设置主机名等(其他功能均正常)
更多推荐
所有评论(0)