目录

自动化需求

代码

使用方法


自动化需求

根据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地址,无法设置主机名等(其他功能均正常)

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐