vmware / pyvmomi

VMware vSphere API Python Bindings
Apache License 2.0
2.19k stars 766 forks source link

Through vim.vm.CloneSpec() cloning failed #1073

Closed snow-with-tea closed 1 week ago

snow-with-tea commented 2 months ago

Describe the bug

I have a virtual machine named "template_centos7", and now I want to clone it, but I have failed using the following script

Did I make a mistake in writing somewhere

image

import logging
import re
import sys
import random
import time
from pyVim.connect import SmartConnect, Disconnect
from pyVmomi import vim
import ssl

class exsi:
    def __init__(self, host="127.0.0.1", user="root", pwd="12345678", ):
        # 禁用 SSL 证书验证(不推荐在生产环境中使用)
        sslContext = ssl.create_default_context()
        sslContext.check_hostname = False
        sslContext.verify_mode = ssl.CERT_NONE

        # 连接到 exsi
        self.si = SmartConnect(host=host, user=user, pwd=pwd, sslContext=sslContext)
        # 检索内容
        self.content = self.si.RetrieveContent()
        for child in self.content.rootFolder.childEntity:
            print(child.name)
            vm_folder = child.vmFolder  # child is a datacenter
        # rootFolder 是 vCenter Server 中所有对象的根目录,包括虚拟机、主机、数据中心等。我们将这个根目录赋值给 container,以便稍后在容器视图中使用
        container = self.content.rootFolder
        # vim.VirtualMachine 是 PyVmomi 中表示虚拟机的类
        # 创建了一个列表 view_type, 其中包含我们感兴趣的对象类型, 这里只填写了虚拟机类型
        view_type = [vim.VirtualMachine]
        # recursive 是一个布尔值, 表示是否递归地检索容器中的对象
        recursive = True
        # 创建容器视图, 三个参数分别为:
        #     container:容器视图的根节点
        #     view_type:感兴趣的对象类型, 即虚拟机。
        #     recursive:是否递归地检索对象。
        self.container_view = self.content.viewManager.CreateContainerView(container, view_type, recursive)

    def get_attr(self, obj, obj_attrs: str, default=None):
        """
        getattr不知为何不能获取vm的多层属性, 此函数模仿getattr获取类的多层属性, 以小数点为分隔
        本函数不支持获取不到属性时报错退出, 只能返回一个默认值, 这个值默认为空
        :param obj: 要解析的对象
        :param obj_attrs: 对象的属性, 多层之间以小数点分隔
        :param default: 没有找到属性时的默认值, 可以不填, 不填为空
        """
        list_attrs = re.split('\\.', obj_attrs)
        obj_init = obj
        for i in range(0, len(list_attrs)):
            obj_init = getattr(obj_init, list_attrs[i], None)
            # print(obj_init)
            if not obj_init:
                obj_init = default
                break
        return obj_init

    def generate_mac(self):
        """
        生成新的mac地址
        """
        random.seed(time.time())
        # 生成MAC地址的前三个字节(厂商标识)
        oui = [random.randint(0x00, 0x7f) for _ in range(3)]
        # 生成MAC地址的后三个字节(设备标识)
        nic = [random.randint(0x00, 0xff) for _ in range(3)]
        # 将字节转换为十六进制字符串并连接起来
        mac_address = ':'.join(['{:02x}'.format(b) for b in oui + nic]).upper()
        return mac_address

    def wait_for_task(self, task):
        """
        等待exsi任务执行结束并获取结果
        """
        task_done = False
        # 死循环检测task的状态
        while not task_done:
            # logging.info(f"task.....{task.info.state} ")
            if task.info.state == 'success':
                logging.info(f"监听到exsi的任务执行成功")
                return "ok"

            if task.info.state == 'error':
                logging.error(f"监听到exsi的任务执行失败了")
                return task.info.error.msg

    # 获取指定虚拟机的信息
    def get_vm_info_once(self, vm_obj) -> dict:
        """
        获取所有虚拟机信息
        :param vm_obj: CreateContainerView获取的虚拟机对象
        """
        # 定义取哪些关键信息
        vm_properties = {
            "name_VM": "name",
            "uuid": "config.uuid",
            "CPU": "config.hardware.numCPU",
            "memory_MB": "config.hardware.memoryMB",
            "name_OS": "config.guestFullName",
            "status_tools": "guest.toolsRunningStatus",
            "IP": "guest.ipAddress",
            "status_power": "summary.runtime.powerState",
        }
        res = {}
        for k, v in vm_properties.items():
            # print(dir(vm_obj))
            # res[k] = getattr(vm_obj, "config.uuid")
            res[k] = self.get_attr(vm_obj, v)
        return res

    # 返回虚拟机指定的指标信息, 可指定名字也可以全要
    def get_vm_info_all(self, vm_name: str = None, vm_name_list: list = None) -> dict:
        """
        返回虚拟机指定的指标信息, 可指定名字也可以全要
        :param vm_name: 要查的虚拟机名字
        :param vm_name_list: 要查的虚拟机名字的列表
        """
        res = {}
        for vm in self.container_view.view:
            if vm_name:
                if vm.name != vm_name:
                    continue
            if vm_name_list:
                if vm.name not in vm_name_list:
                    continue
            res[vm.name] = self.get_vm_info_once(vm)
            # print(dir(vm.config.hardware))
        return res

    # 创建虚拟机
    def clone_vm(self, template_name: str, vm_name: str, vm_ip: str, vm_cpu: int = None, vm_member_mb: int = None):
        """
        :param template_name: 被克隆虚拟机的名字
        :param vm_name: 要新建的虚拟机的名字
        :param vm_ip: 要新建的虚拟机的IP
        :param vm_cpu: 要新建的虚拟机的cpu数量
        :param vm_member_mb: 要新建的虚拟机的内存大小, 单位: MB
        :param
        :param
        :return:
        """

        # 先定义这个被克隆的虚拟机实例为空
        template_vm = None
        template_n = 0
        # 寻找这个虚拟机
        for vm in self.container_view.view:
            if vm.name == template_name:
                template_vm = vm
                template_n += 1
        # 没找到就退出
        if not template_vm:
            return f"没找到名为 '{template_name}' 的虚拟机"
        # 如果获取到不止一个, 说明有重名的虚拟机, 立即退出报错
        if template_n != 1:
            return f"发现不止一个名为 '{template_name}' 的已存在虚拟机"

        # 获取这个模板的相关信息
        # template_info_tmp = self.get_vm_info_once(template_vm)
        # logging.info(template_info_tmp)
        template_info = self.get_vm_info_once(template_vm)
        # 如果没有传入某些配置, 那么保持和模板一致
        if not vm_cpu:
            vm_cpu = template_info["CPU"]
            logging.info(f"未指定cpu核数, 沿用模板虚拟机的: {vm_cpu}")
        if not vm_member_mb:
            vm_member_mb = template_info["memory_MB"]
            logging.info(f"未指定内存大小(MB), 沿用模板虚拟机的: {vm_member_mb}")

        # 获取当前所有虚拟机信息
        vm_info_all = self.get_vm_info_all()
        # 校验传入的某些参数是否和现有虚拟机冲突
        for k, v in vm_info_all.items():
            # 校验IP地址是否重复
            if vm_ip == v["IP"]:
                return f"指定的新IP:{vm_ip}与虚拟机:{v['name_VM']}的IP:{v['IP']}冲突"

        # 新建
        # 获取模版
        template = template_vm
        datacenter_name = "ha-datacenter"
        datastore_name = None
        cluster_name = None

        # 开始克隆
        clone_spec = vim.vm.CloneSpec()
        clone_spec.location = vim.vm.RelocateSpec()
        clone_spec.powerOn = False
        clone_spec.template = False

        # Clone the VM
        clone_task = template_vm.CloneVM_Task(folder=template_vm.parent, name=vm_name, spec=clone_spec)
        res = self.wait_for_task(clone_task)
        return res

if __name__ == '__main__':
    # 定义日志格式
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s:%(filename)s:%(funcName)s:%(thread)d:%(lineno)d %(message)s"
    )
    a = exsi()
    # b = a.get_datastore_by_name("a", "ha-datacenter")
    b = a.clone_vm(
        template_name="template_centos7",
        vm_name="test-new-1",
        vm_ip="10.33.22.10"
    )
    logging.info(b)

Reproduction steps

  1. directly run the script

Expected behavior

I hope to clone another virtual machine from an existing one

Additional context

No response

prziborowski commented 2 months ago

The CloneVm method only works from vCenter. You can't execute it directly from ESXi.

snow-with-tea commented 2 months ago

The CloneVm method only works from vCenter. You can't execute it directly from ESXi.

Is there any other way to achieve this effect? Creating one by one is very painful

prziborowski commented 2 months ago

If your template VM is very simple, then it is possible you could re-implement what clone method is doing (copying files and creating a new VM). I haven't done something like this in many years, so I don't know how well it still works. There is a VirtualDiskManager.CopyVirtualDisks

So from your esxi class, you'd call:

from pyVim.task import WaitForTask
task = self.content.virtualDiskManager.CopyVirtualDisks(sourceName=sourceName, destName=destName)
WaitForTask(task)

And once the disks are copied, you can use this create_vm.py sample: https://github.com/vmware/pyvmomi-community-samples/pull/523/files for an idea on attaching existing disks for a new VM. The other tricky part would be creating a ConfigSpec that looks similar to the ConfigInfo of the source VM, but replacing identifiers so that you have a unique VM. I don't have any pointers to any code that does that.

Alternatively I believe you can export the VM (as an ovf/ova), and import it to get a new VM. This won't be very performant though as it would be transferring disks through your client. For that, you would combine https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/export_vm.py and https://github.com/vmware/pyvmomi-community-samples/blob/master/samples/deploy_ovf.py Or could leverage the ovftool binary that I believe Workstation and Fusion have.

If you are just using the same template over and over for the clones, you could host the ovf/vmdk on a webserver that ESXi has access to (might have to configure firewall rules for that) and the deployOvf/deployOva can be used to pull those files.

The easiest way is to use vCenter though.