vmware的python自动化:批量克隆虚拟机

目录

自动化需求

代码

使用方法


自动化需求

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

相关推荐
稳联技术老娜3 小时前
ModbusTCP转Profinet网关配置要点——助力汽车生产线能效优化
自动化·汽车·制造
kim_puppy3 小时前
网络初识相关
运维·服务器·网络
Johnstons3 小时前
2026网络流量监控分析工具深度对比与选型指南
运维·网络·网络流量分析
努力的lpp3 小时前
小迪安全第8天:基础入门-算法分析 & 传输加密 & 数据格式 & 密文存储 & 代码混淆 & 逆向保护
服务器·网络·apache
禹笑笑-AI食用指南3 小时前
一个本地 OpenClaw 自动化项目的架构难点与解决方案
运维·架构·自动化·openclaw·龙虾
Ulyanov3 小时前
基于ttk的Python现代化GUI开发指南
开发语言·前端·python·tkinter·系统设计
白云偷星子3 小时前
云原生笔记7
linux·运维·redis·笔记·云原生
xiaohe073 小时前
nginx 代理 redis
运维·redis·nginx
同聘云3 小时前
阿里云国际服务器动态IP连不上是怎么回事?服务器的ip地址怎么查?
服务器·tcp/ip·阿里云