自动化需求

获取集群内虚拟机的信息,便于统计以及查询(支持多集群)

版本:
python3.11
pyvmomi 9.0.0.0
pyvim 3.0.3

代码

import ssl
import datetime
import concurrent.futures
import pandas as pd
from pyVim.connect import SmartConnect, Disconnect
from pyVmomi import vim
from pyVmomi import vmodl

# ================= 配置区域 =================
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'
    # }
}

# 导出文件名称
today = datetime.datetime.now().strftime("%Y%m%d")
OUTPUT_FILE = f"虚拟机巡检_{today}.xlsx"


# =======================================================================


def create_property_filter_spec(container, vim_type, properties):
    """创建 PropertyCollector 过滤器规范"""
    obj_spec = vmodl.query.PropertyCollector.ObjectSpec()
    obj_spec.obj = container
    obj_spec.selectSet = [
        vmodl.query.PropertyCollector.TraversalSpec(
            name='traverseEntities',
            path='view',
            skip=False,
            type=vim.view.ContainerView
        )
    ]

    prop_spec = vmodl.query.PropertyCollector.PropertySpec()
    prop_spec.type = vim_type
    prop_spec.pathSet = properties

    filter_spec = vmodl.query.PropertyCollector.FilterSpec()
    filter_spec.objectSet = [obj_spec]
    filter_spec.propSet = [prop_spec]
    return filter_spec


def get_cluster_name(host_mor, cluster_cache):
    """获取集群名称"""
    if not host_mor:
        return ''

    if host_mor in cluster_cache:
        return cluster_cache[host_mor]

    try:
        parent = host_mor.parent
        while parent:
            if isinstance(parent, vim.ClusterComputeResource):
                cluster_name = parent.name
                cluster_cache[host_mor] = cluster_name
                return cluster_name
            if hasattr(parent, 'parent'):
                parent = parent.parent
            else:
                break
        cluster_cache[host_mor] = ''
        return ''
    except Exception:
        cluster_cache[host_mor] = ''
        return ''


def calculate_uptime_days(vm, power_state, boot_time):
    """
    【核心修复】多重方式计算虚拟机运行天数
    优先使用 bootTime,失败时尝试其他方式
    """
    if power_state != 'poweredOn':
        return 0

    # 方式1:使用 runtime.bootTime(首选)
    if boot_time:
        try:
            if boot_time.tzinfo:
                boot_time = boot_time.replace(tzinfo=None)
            now = datetime.datetime.now()
            delta = now - boot_time
            days = round(delta.total_seconds() / 86400, 1)
            if days > 0:
                return days
        except Exception as e:
            pass

    # 方式2:如果 bootTime 失败,尝试从 guest 获取信息
    try:
        if hasattr(vm, 'guest') and hasattr(vm.guest, 'toolsRunningStatus'):
            # 如果 tools 正在运行,我们可以尝试其他方式
            # 这里作为备选方案,暂时返回 None 表示无法获取
            pass
    except Exception:
        pass

    # 方式3:尝试使用 config.modified(最后修改时间,仅作参考)
    try:
        if hasattr(vm, 'config') and hasattr(vm.config, 'modified'):
            modified_time = vm.config.modified
            if modified_time:
                if modified_time.tzinfo:
                    modified_time = modified_time.replace(tzinfo=None)
                now = datetime.datetime.now()
                delta = now - modified_time
                days = round(delta.total_seconds() / 86400, 1)
                # 注意:modified 不是启动时间,仅作为兜底
                return days
    except Exception:
        pass

    # 如果所有方式都失败,返回 None(在Excel中显示为空)
    return None


def get_vm_ip(guest_net, guest_ip):
    """获取IP地址"""
    try:
        if guest_ip and guest_ip != '127.0.0.1':
            return guest_ip

        if guest_net:
            for nic in guest_net:
                if hasattr(nic, 'ipAddress') and nic.ipAddress:
                    for ip in nic.ipAddress:
                        if ip and not ip.startswith('127.') and not ip.startswith('169.254.'):
                            return ip
        return ''
    except Exception:
        return ''


def get_datastore_names(datastores):
    """获取存储名称列表"""
    try:
        if not datastores:
            return ''
        ds_names = sorted(list({ds.name for ds in datastores}))
        return ', '.join(ds_names)
    except Exception:
        return ''


def get_vm_tools_status(tools_status):
    """Tools状态映射"""
    status_map = {
        'toolsNotInstalled': '未安装',
        'toolsNotRunning': '未运行',
        'toolsOld': '版本过旧',
        'toolsOk': '正常',
        'toolsBlacklisted': '已黑名单'
    }
    return status_map.get(tools_status, '')


def get_power_state(power_state):
    """电源状态映射"""
    state_map = {
        'poweredOn': '已开机',
        'poweredOff': '已关机',
        'suspended': '已挂起'
    }
    return state_map.get(power_state, '')


def process_single_vc(vc_ip, vc_cred):
    """使用 PropertyCollector 处理单个 vCenter"""
    print(f"正在连接 vCenter: {vc_ip} ...")
    si = None
    context = ssl._create_unverified_context()
    all_vm_info = []
    cluster_cache = {}
    debug_count = {'boot_time_null': 0, 'total': 0}

    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}")

        # 创建 ContainerView
        print(f"正在创建虚拟机视图...")
        container = content.viewManager.CreateContainerView(
            content.rootFolder, [vim.VirtualMachine], True
        )

        # 【修改】增加 config.modified 作为备选时间
        properties = [
            'name',
            'runtime.host',
            'runtime.powerState',
            'runtime.bootTime',
            'guest.ipAddress',
            'guest.net',
            'guest.toolsStatus',
            'config.hardware.memoryMB',
            'config.hardware.numCPU',
            'config.instanceUuid',
            'config.annotation',
            'config.modified',  # 新增:最后修改时间作为备选
            'datastore',
            'summary.storage.committed'
        ]

        # 创建 PropertyCollector 过滤器
        filter_spec = create_property_filter_spec(container, vim.VirtualMachine, properties)

        print(f"🚀 开始批量获取虚拟机属性 (PropertyCollector)...")
        pc = content.propertyCollector
        result = pc.RetrieveProperties(specSet=[filter_spec])

        container.Destroy()
        print(f"📊 {vc_ip} 共获取 {len(result)} 台虚拟机数据")

        # 解析结果
        print(f"正在解析数据...")
        for i, obj_content in enumerate(result, 1):
            try:
                # 将属性转换为字典
                props = {p.name: p.val for p in obj_content.propSet}

                # 获取主机和集群
                host_mor = props.get('runtime.host')
                host_name = host_mor.name if host_mor else ''
                cluster_name = get_cluster_name(host_mor, cluster_cache) if host_mor else ''

                # 获取电源状态
                power_state = props.get('runtime.powerState')

                # 【核心修复】计算运行天数
                debug_count['total'] += 1
                boot_time = props.get('runtime.bootTime')

                if power_state == 'poweredOn' and not boot_time:
                    debug_count['boot_time_null'] += 1

                # 传递完整 props 给计算函数
                uptime_days = calculate_uptime_days(
                    None,  # 这里不需要完整 vm 对象
                    power_state,
                    boot_time
                )

                # 如果 bootTime 失败,尝试使用 config.modified
                if uptime_days is None and power_state == 'poweredOn':
                    modified_time = props.get('config.modified')
                    if modified_time:
                        try:
                            if modified_time.tzinfo:
                                modified_time = modified_time.replace(tzinfo=None)
                            now = datetime.datetime.now()
                            delta = now - modified_time
                            uptime_days = round(delta.total_seconds() / 86400, 1)
                        except Exception:
                            pass

                # 计算已用空间(GB)
                committed_bytes = props.get('summary.storage.committed', 0)
                used_space_gb = round(committed_bytes / (1024 ** 3), 2) if committed_bytes else 0

                # 构建信息字典
                vm_info = {
                    'vCenter IP': vc_ip,
                    '虚拟机名称': props.get('name', ''),
                    '所在集群': cluster_name,
                    '所在主机': host_name,
                    '电源状态': get_power_state(power_state),
                    'IP地址': get_vm_ip(props.get('guest.net'), props.get('guest.ipAddress')),
                    '内存大小(GB)': round(props.get('config.hardware.memoryMB', 0) / 1024, 2),
                    'CPU数量': props.get('config.hardware.numCPU', 0),
                    'UUID': props.get('config.instanceUuid', ''),
                    '备注信息': props.get('config.annotation', '').strip() if props.get('config.annotation') else '',
                    'vmtools状态': get_vm_tools_status(props.get('guest.toolsStatus')),
                    '运行天数': uptime_days,  # 可能是数字或None
                    '存储名称': get_datastore_names(props.get('datastore')),
                    '已用空间(GB)': used_space_gb
                }
                all_vm_info.append(vm_info)

                if i % 1000 == 0:
                    print(f"   {vc_ip} 已解析: {i}/{len(result)}")

            except Exception as e:
                print(f"   ⚠️  解析第 {i} 台虚拟机时出错: {str(e)}")
                continue

        # 打印调试信息
        print(f"\n📊 {vc_ip} 运行天数统计:")
        print(f"   总计虚拟机: {debug_count['total']}")

        print(f"✅ {vc_ip} 处理完成,共收集 {len(all_vm_info)} 台")

    except Exception as e:
        print(f"❌ 处理 vCenter {vc_ip} 失败: {str(e)}")
        import traceback
        traceback.print_exc()
    finally:
        if si:
            Disconnect(si)
            print(f"已断开 vCenter {vc_ip} 连接\n")

    return all_vm_info


def main():
    print("=" * 60)
    print("🚀 多vCenter虚拟机信息收集工具")
    print("=" * 60)

    start_time = datetime.datetime.now()
    total_vm_list = []

    # 多 vCenter 并发处理
    print(f"\n开始处理 {len(VC_CONFIGS)} 个 vCenter...")
    with concurrent.futures.ThreadPoolExecutor(max_workers=len(VC_CONFIGS)) as executor:
        future_to_vc = {
            executor.submit(process_single_vc, vc_ip, vc_cred): vc_ip
            for vc_ip, vc_cred in VC_CONFIGS.items()
        }

        for future in concurrent.futures.as_completed(future_to_vc):
            vc_ip = future_to_vc[future]
            try:
                vc_vm_list = future.result()
                total_vm_list.extend(vc_vm_list)
            except Exception as e:
                print(f"❌ vCenter {vc_ip} 处理异常: {str(e)}")

    # 导出到Excel
    if total_vm_list:
        print(f"\n📝 正在导出 {len(total_vm_list)} 台虚拟机信息到 {OUTPUT_FILE} ...")
        df = pd.DataFrame(total_vm_list)

        # 按要求的列顺序排列
        column_order = [
            'vCenter IP', '虚拟机名称', '所在集群', '所在主机',
            '电源状态', 'IP地址', '内存大小(GB)', 'CPU数量',
            'UUID', '备注信息', 'vmtools状态', '运行天数', '存储名称', '已用空间(GB)'
        ]
        df = df[column_order]

        # 使用 openpyxl 引擎
        with pd.ExcelWriter(OUTPUT_FILE, engine='openpyxl') as writer:
            df.to_excel(writer, index=False, sheet_name='虚拟机清单')

            # 调整列宽
            worksheet = writer.sheets['虚拟机清单']
            column_widths = {
                'A': 15, 'B': 30, 'C': 20, 'D': 25, 'E': 10,
                'F': 18, 'G': 12, 'H': 10, 'I': 35, 'J': 30,
                'K': 12, 'L': 12, 'M': 40, 'N': 15
            }
            for col, width in column_widths.items():
                worksheet.column_dimensions[col].width = width

        end_time = datetime.datetime.now()
        elapsed = (end_time - start_time).total_seconds()
        print(f"✅ 导出完成!文件: {OUTPUT_FILE}")
        print(f"⏱️  总耗时: {elapsed:.2f} 秒")
    else:
        print("⚠️  未收集到任何虚拟机信息")


if __name__ == "__main__":
    main()

使用方法

在代码内填入vc的有关信息,然后运行脚本即可得到虚拟机信息的xlsx文件

200台虚拟机获取速度约1s

获取的信息包括vCenter IP    虚拟机名称    所在集群    所在主机    电源状态    IP地址    内存大小(GB)    CPU数量    UUID    备注信息    vmtools状态    运行天数    存储名称    已用空间(GB)

效果如下图:

脚本为ai编写,使用前请使用测试环境进行测试

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐