目录
自动化需求
根据xlsx表格信息批量克隆虚拟机(支持多硬盘,支持IP信息设置,支持自定义规范设置)
版本:
python3.11
pyvmomi 9.0.0.0
pyvim 3.0.3
代码
python
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地址,无法设置主机名等(其他功能均正常)