背景 在生产实践中,需要对 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"]))