Skip to content

使用 PyVmomi 获取 VMWare 虚拟化集群信息

Published: at 10:00 AM

Table of contents

Open Table of contents

背景

在生产实践中,需要对 VMWare 虚拟化集群进行监控,并对其中的虚拟机进行自动化管理。

PyVmomi 作为 VMWare 官方提供的开源 Python SDK,它提供了丰富的 API 接口,便于开发者获取集群信息、虚拟机信息等,以及对虚拟机进行各种操作。

信息获取

基本方法

在初始化与 VMWare vCenter 的连接的过程中,需要注意的一点是,自 PyVmomi v8.0 起,connect.ConnectNoSSL() and connect.SmartConnectNoSSL() 方法已被移除,解决方式是在常规的连接方法中增加 disableSslCertValidation=True 选项。

def init_connection(vc_ip, username, password):
    """
    初始化与 vCenter 的连接

    :param vc_ip: vCenter 的 IP 地址
    :param username: 登录用户名
    :param password: 登录密码
    :return: vCenter 的内容对象
    """
    service_instance = connect.Connect(
        host=vc_ip, port=443, user=username, pwd=password, disableSslCertValidation=True
    )
    atexit.register(connect.Disconnect, service_instance)
    content = service_instance.RetrieveContent()
    return content
def get_obj(content, vimtype, name=None):
    """
    从 vc 的内容中获取指定类型的对象列表

    :param content: vCenter 内容对象
    :param vimtype: 要获取的对象类型
    :param name: 对象名称,可选
    :return: 匹配的对象列表
    """
    container = content.viewManager.CreateContainerView(
        content.rootFolder, vimtype, True
    )

    if name is not None:
        objects = [view for view in container.view if name and view.name == name]
    else:
        objects = [view for view in container.view]

    return objects
# 连接到 VC 获取相关信息
vc_content = init_connection(vc_ip, username, password)

# 一些常用对象的获取
host_obj = get_obj(vc_content, [vim.HostSystem])
vm_obj = get_obj(vc_content, [vim.VirtualMachine])
cluster_obj = get_obj(vc_content, [vim.ClusterComputeResource])
ds_obj = get_obj(vc_content, [vim.Datastore])

获取宿主机信息

from math import ceil

def query_host_info(esxi):
    """
    查询 ESXi 主机的信息 (vimtype = 'HostSystem')

    :param esxi: ESXi 主机对象
    :return: ESXi 主机的信息
    """
    host_info = {
        "ip": esxi.name,
        "vc_ip": esxi.summary.managementServerIp,
        "in_maintenance_mode": esxi.runtime.inMaintenanceMode,
        "processor_usage/%": "%.1f"
        % (
            esxi.summary.quickStats.overallCpuUsage
            / (
                esxi.summary.hardware.numCpuPkgs
                * esxi.summary.hardware.numCpuCores
                * esxi.summary.hardware.cpuMhz
            )
            * 100
        ),  # 处理器使用率
        "memory/GB": ceil(esxi.summary.hardware.memorySize / (1024**3)),  # 内存 (GB)
        "memory_usage/%": "%.1f"
        % (
            (
                esxi.summary.quickStats.overallMemoryUsage
                / (esxi.summary.hardware.memorySize / (1024**2))
            )
            * 100
        ),  # 内存使用率
        "cpu_total_cores": esxi.hardware.cpuInfo.numCpuCores,
        "from_cluster": esxi.parent.name,
    }

    return host_info

获取虚拟机信息

在获取虚拟机的磁盘信息时,虚拟磁盘的名称和大小并不是我们关注的重点。除了直观的计算出当前虚拟机的磁盘总容量外,通过格式化输出虚拟机分配的数据存储 LUN 的名称列表,可以便捷的对某一特定存储所关联的虚拟机进行筛选,进而对虚拟机所关联的业务、软件应用等进行归类管理。

def calculate_vm_disk_size(vm):
    """
    计算虚拟机磁盘大小
    """
    vm_disk_size = sum(d.capacityInKB / (1024**2) for d in vm.config.hardware.device if isinstance(d, vim.vm.device.VirtualDisk))
    # 去重
    from_lun = {d.backing.datastore.info.name for d in vm.config.hardware.device if isinstance(d, vim.vm.device.VirtualDisk)}
    # 排序
    sorted_from_lun = sorted(from_lun, key=str)
    # 格式化输出
    formatted_from_lun = ", ".join(map(str, sorted_from_lun))
    return vm_disk_size, formatted_from_lun
def extract_ip(vm):
    """
    提取虚拟机的IP地址
    """
    ip = ""
    try:
        if vm.guest.ipAddress:
            # 遍历网卡
            for nic in vm.guest.net:
                address = nic.ipConfig.ipAddress
                for addr in address:
                    tip = addr.ipAddress
                    # 只筛选 10、122、172 开头的
                    if tip.startswith(("10.", "172.", "122.")):
                        return tip
    except Exception as e:
        logging.error(f"Error while extracting IP for VM {vm.name}")
        logging.error(e)
    return ip
def query_vm_info(vm, vc_ip):
    """
    查询虚拟机的信息
    """
    try:
        vm_disk_size, formatted_from_lun = calculate_vm_disk_size(vm)
    except Exception:
        logging.error(f"Error while calculating disk size for VM {vm.name}")
        return

    ip = extract_ip(vm)

    try:
        mem = getattr(vm.config.hardware, 'memoryMB', 0)
    except Exception:
        logging.error(f"Error while getting mem for VM {vm.name}")
        mem = 0

    try:
        cpu_num = vm.config.hardware.numCPU
    except Exception:
        logging.error(f"Error while getting CPU for VM {vm.name}")
        cpu_num = 0

    vm_info = {
        "name": vm.name,  # vm-vmw73491-apc
        "ip": ip,  # 需要服务器开机后才可以获取
        "vc_ip": vc_ip, # vCenter 的 IP 地址
        "vm_name": vm.config.name,  # vm-vmw73491-apc
        "power_state": vm.runtime.powerState,  # poweredOn
        "cpu_num": cpu_num,  # 2
        "memory": mem,  # 8192 MB
        "disk_size": ceil(vm_disk_size),  # 所有虚拟磁盘容量相加 GB
        "from_host": vm.runtime.host.name,  # 所属的宿主机
        "from_host_name": vm.runtime.host.config.network.dnsConfig.hostName, # 所属虚拟机的主机名
        "from_cluster": vm.runtime.host.parent.name,  # 所属的 Cluster
        "from_lun": formatted_from_lun,  # 每个虚拟磁盘的 lun,去重排序
    }

    return vm_info

获取集群信息

from math import ceil

def query_cluster_info(cluster, vc_ip):
    """
    创建 Cluster 视图,返回 Cluster 关键信息

    :param cluster: 所需查询的 cluster_obj
    :param vc_ip: 当前 VC IP
    :return: 当前 Cluster 的信息
    """
    # 获取主机和虚机的列表
    host_list = cluster.host
    host_num = cluster.summary.numHosts
    vm_list = cluster.resourcePool.vm
    vm_num = len(vm_list)

    # 计算超授比
    oversub_ratio = round(vm_num / host_num) if host_num != 0 else 0

    # 计算 CPU 数
    cpu_num = cluster.summary.numCpuCores

    # 计算已分配 CPU 数
    assigned_cpu_num = 0
    for vm in vm_list:
        try:
            assigned_cpu_num += vm.config.hardware.numCPU
        except AttributeError:
            assigned_cpu_num += 0

    # 计算 CPU 超授比
    cpu_oversub_ratio = (
        round(assigned_cpu_num / cpu_num, 1) if assigned_cpu_num != 0 else 0
    )

    # 用于计算 CPU 使用率
    cpu_used = sum(host.summary.quickStats.overallCpuUsage or 0 for host in host_list)

    # 计算内存总量 / 已分配内存 / 已使用内存
    total_memory = ceil(cluster.summary.totalMemory / (1024**3))
    total_assigned_memory = 0
    for vm in vm_list:
        try:
            total_assigned_memory += int(vm.config.hardware.memoryMB / 1024)
        except AttributeError:
            total_assigned_memory += 0
    total_used_memory = sum(
        int((host.summary.quickStats.overallMemoryUsage or 0) / 1024)
        for host in cluster.host
    )

    # 计算内存超授比
    mem_oversub_ratio = (
        round(total_assigned_memory / total_memory, 1)
        if total_assigned_memory != 0
        else 0
    )

    # 计算容量 / 剩余空间
    capacity = freespace = 0
    for ds in cluster.datastore:
        if ds.summary.multipleHostAccess is True:
            capacity += ds.summary.capacity
            freespace += ds.summary.freeSpace
            freespace -= (
                ds.summary.uncommitted if ds.summary.uncommitted is not None else 0
            )

    # 构建 cluster_info dict
    cluster_info = {
        "name": cluster.name, # 集群名
        "vc_ip": vc_ip, # vCenter 的 IP 地址
        "host_num": host_num,  # 主机数
        "vm_num": vm_num,  # 虚机数
        "oversub_ratio": f"1:{oversub_ratio}" if oversub_ratio != 0 else "1:1",  # 超授比
        "cpu_num": cpu_num,  # CPU 数
        "assigned_cpu_num": assigned_cpu_num,  # 已分配 CPU 数
        "cpu_oversub_ratio": f"1:{cpu_oversub_ratio}"
        if cpu_oversub_ratio != 0
        else "N/A",  # CPU 超授比
        "cpu_usage/%": "%.1f" % (cpu_used / cluster.summary.totalCpu * 100),  # CPU 使用率
        "total_memory/GB": total_memory,  # 总内存/GB
        "total_assigned_memory/GB": total_assigned_memory,  # 已分配内存/GB
        "total_used_memory/GB": total_used_memory,  # 已使用内存/GB
        "mem_oversub_ratio": f"1:{mem_oversub_ratio}"
        if mem_oversub_ratio != 0
        else "1:0.0",  # 内存超授比
        "mem_usage/%": "%.1f" % (total_used_memory / total_memory * 100),  # 内存使用率
        "capacity/TB": ceil(capacity / (1024**4)),  # TB
        "freespace/GB": int(freespace / (1024**3)),  # GB
        "uncommitted/GB": int(uncommitted / (1024**3)),  # GB
        "storage_usage/%": "%.1f" % ((1 - freespace / capacity) * 100) if capacity != 0 else 0,
    }

    return cluster_info

获取数据存储信息

由于从数据存储对象无法直接获取其所属的 Cluster,因此需要先构建一个 Cluster 和 LUN 相对应的字典,在遍历数据存储对象的时候从字典中反查得到其所属的集群。

def init_cluster_lun(cluster_obj):
    """
    初始化 Cluster 和 LUN 对应的字典

    :param cluster_obj: cluster 列表
    :return: None
    """
    global cluster_lun

    for cluster in cluster_obj:
        for ds in cluster.datastore:
            cluster_lun[ds.summary.name] = cluster.name
def query_datastore_info(ds, vc_ip):
    """
    查询数据存储的信息 (vimtype = 'Datastore')

    :param ds: 数据存储对象
    :param vc_ip: 当前 VC IP
    :return: 数据存储的信息
    """
    if ds.summary.multipleHostAccess is False:
        # print("跳过本地磁盘:", ds.summary.name)
        return

    uncommitted = int(ds.summary.uncommitted / (1024**3)) if ds.summary.uncommitted is not None else 0

    ds_info = {
        "name": ds.summary.name,
        "vc_ip": vc_ip,
        "capacity/GB": ceil(ds.summary.capacity / (1024**3)),
        "freespace/GB": int(ds.summary.freeSpace / (1024**3)) - uncommitted,
        "from_cluster": cluster_lun.get(ds.summary.name, ""),
    }

    return ds_info

数据展示

输出到控制台

为了在开发过程中便于调试,可以将获取到的信息视图输出到控制台。以数据存储信息为例:

# 输出数据存储信息
for i, ds in enumerate(ds_obj):
    ds_info = query_datastore_info(ds, vc_ip)
    if ds_info is not None:
        print(
            json.dumps(
                ds_info,
                indent=4,
                ensure_ascii=False,
                default=str,
            )
        )
    if i == 5:
        break

输出到表格文件

为了将多个信息视图展示在一个 Excel 文件中,在处理每个信息视图的过程中,可以对目标 Excel 工作表的名称进行指定,这样不同类的信息视图可以分别存储在不同的 tab 中。

import openpyxl

def write_dict_list_to_excel(dict_list, sheet_title):
    """
    将字典列表写入 Excel 文件

    :param dict_list: 包含字典元素的列表
    :param sheet_title: 工作表标题
    :return: None
    """
    # 检查工作表是否已存在
    if sheet_title in workbook.sheetnames:
        # 获取已存在的工作表
        sheet = workbook[sheet_title]
    else:
        # 创建新的工作表
        sheet = workbook.create_sheet(title=sheet_title)

        # 将字典列表的 key 作为表头写入第一行
        headers = list(dict_list[0].keys())
        sheet.append(headers)

    # 将字典列表的 value 写入 Excel 文件
    for data in dict_list:
        sheet.append(list(data.values()))

在处理表格文件输出时,首先需要完成如下步骤:

# 创建新的工作簿
workbook = openpyxl.Workbook()
sheet = workbook.active

# 删除默认的 Sheet 工作表
if "Sheet" in workbook.sheetnames:
    workbook.remove(workbook["Sheet"])

分别处理每个信息视图:

# 输出宿主机信息
host_list = []
for obj in host_obj:
    host_list.append(query_host_info(obj))
write_dict_list_to_excel(host_list, "宿主机信息")

# 输出虚拟机信息
vm_list = []
for obj in vm_obj:
    vm_list.append(query_vm_info(obj, vc_ip))
write_dict_list_to_excel(vm_list, "虚拟机信息")

# 输出集群信息
cluster_list = []
for obj in cluster_obj:
    cluster_list.append(query_cluster_info(obj, vc_ip))
write_dict_list_to_excel(cluster_list, "集群信息")

# 输出数据存储信息
# 创建 Cluster 和 datastore (LUN_name) 对应的字典
init_cluster_lun(cluster_obj)
ds_list = []
for obj in ds_obj:
    ds_list.append(query_datastore_info(obj, vc_ip))
write_dict_list_to_excel(ds_list, "数据存储信息")

# 将表格存储到磁盘
workbook.save("vc_info.xlsx")

输出到数据库

采用 psycopg 连接到 PostgreSQL 数据库,将信息视图存储到数据库中。

import psycopg

# 数据库连接信息
db_name = "db_name"
db_user = "db_user"
db_pass = "db_pass"
db_host = "db_host"
db_port = "db_port"

# 数据库连接字符串
conn_info = f"dbname={db_name} user={db_user} password={db_pass} host={db_host} port={db_port}"

# 插入数据库表中
with psycopg.connect(conn_info) as conn:
    with conn.cursor() as cur:
        cur.execute("""
            INSERT INTO db.table_name
            VALUES
            (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
            """, (row["name"], row["ip"], row["vc_ip"], row["power_state"], \
                row["cpu_num"], row["memory"], row["disk_size"], row["from_host"], \
                row["from_host_name"], row["from_cluster"], row["from_lun"]))