使用 vsphere-automation-sdk-python 自动创建虚拟机

想自动分 VMware 虚拟机,所以抽空研究了下它的 Python api。VMware api 本质上是 restapi,只不过有多种语言的封装,如果有兴趣可以关注下 VMware 其他语言的实现。

Python api 有 pyvmomi 以及下面要讲到的 vsphere-automation-sdk 这两种,其中 pyvmomi 使用基于 SOAP 的 API 的 vSphere Web 服务 API,是比较老的东西了;而 vsphere-automation-sdk 使用新版基于 REST 的 api,有类似于 Tagging 和 Content Library 这样的新功能。Appliance Management、NSX、VMware Cloud 等 api 只能使用 vsphere-automation-sdk。

简单来说,以后新的功能的实现就在 vsphere automation sdk 上。虽然如此,但是 vsphere automation sdk 未必就一定适合你。而且 vsphere automation sdk 不管你用还是不用,pyvmomi 是一定要使用的

为什么这么说呢?vsphere-automation-sdk-python 由于使用了新的 api,可以使用类似于内容库(关于内容库的内容,接下来会提到)以及 tagging 这样的新功能,但是其他功能就乏善可陈了,比如它对虚拟机本身的管理就没有丝毫办法。

就说一个很现实的问题,使用 automation sdk 虽然可以从内容库中的 ovf 模板创建虚拟机,但是它只能根据模板的配置创建虚拟机,无法修改 cpu、内存、磁盘、ip 等参数(或许是我不知道?),而这些功能恰好 pyvmomi 都能胜任。

因此,在存在多个 vcenter 的环境,可以使用内容库保持多个 vcenter 之间 ovf 模板的一致,这样不管在哪个 vcenter 中创建虚拟机操作都一样。然后创建完虚拟机之后,再使用 pyvmomi 对虚拟机配置进行修改。当然,如果你的环境中只有一个 vcenter,那么完全就用不到 vsphere-automation-sdk,因为使用 pyvmomi 直接克隆机器速度更快,因为它少了从内容库下载模板的时间。

这个文章要讲的是,使用 vsphere automation sdk 从内容库中的 ovf 模板创建虚拟机,然后通过 pyvmomi 对虚拟机配置进行修改。需要注意的是,内容库是 VSphere 6.0 才有的功能,这篇文章基于 6.5。

我们可以先进入其 github 主页。

这个 SDK api 没什么文档,你得看它提供的示例才能对它有所了解。它所有的示例都在 samples 目录,有意思的是,它的示例不是割裂的,而是一个整体,整个示例就是一个 Python 包,你可以通过传递参数给它来实现创建简单的虚拟机等操作。

说实话,示例写的比较烂,坑很多。有些简单的、几行代码能搞定的事情,它能用更复杂的方式帮你实现。有感于此,我就将我研究的东西写出来,让大家少走弯路吧。

本文基于 VSphere 6.5 + Python 3.6。Python 2.7 也能用,差别不大。

首先安装它的 SDK:

git clone https://github.com/vmware/vsphere-automation-sdk-python.git
cd vsphere-automation-sdk-python
pip3 install --upgrade --force-reinstall -r requirements.txt --extra-index-url file:///`pwd`/lib
复制代码

--extra-index-url:默认 pip 会去 pypi.org/simple 下载 Python 包,你还可以指定其他的 URL 进行下载。

网络的问题会导致中断,不要以为是依赖出现问题,要仔细看报错信息。如果是网络问题,多安装几次,一直安装总会成功的。而且它会自动安装 pyvmomi。

列出所有虚拟机

安装完成之后就可以使用了,我们从最简单的开始,登录,然后列出 vCenter 所有虚拟机:

import requests
import urllib3
from vmware.vapi.vsphere.client import create_vsphere_client
session = requests.session()
session.verify = False
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
client = create_vsphere_client(server='IP', username='USER', password='PASSWORD', session=session)
client.vcenter.VM.list()
复制代码

list 列出来的虚拟机最多只能返回 1000 台,超过这个数量它会抛出异常,这时你需要使用过滤器来减少它返回的数量。过滤器可以使用 client.vcenter.VM.FilterSpec 这个类来指定,它可以通过下面这些属性来进行过滤。

vms=None, names=None, folders=None, datacenters=None, hosts=None, clusters=None, resource_pools=None, power_states=None
复制代码

比如通过虚拟机的名称来进行过滤:

client.vcenter.VM.list(client.vcenter.VM.FilterSpec(names={'10.20.6.77_xyz.tomcat.xyz-common-main.01.sit'}))
复制代码

根据集群名称过滤虚拟机

直接输入集群名称给 vm.FilterSpec 过滤器并不能获得任何结果,而是应该先通过集群名称获得类似集群 id 这样的字串(我是这样理解的)输入进去才行。其实不光是集群,在这个 SDK 中,VMware 中的所有概念,包括网络、存储、文件夹、资源池、虚拟机等它都只认 id,而不是它们的名称(我的理解是名称随时都可以变,但是 id 不会变)。

虽然你不能直接将集群名称给它,但是你可以根据集群名称来获取它的 id,具体做法如下:

from com.vmware.vcenter_client import Cluster
# 获取名为 POC 集群的 id
cluster_id = client.vcenter.Cluster.list(Cluster.FilterSpec(names={'POC'}))[0].cluster
client.vcenter.VM.list(client.vcenter.VM.FilterSpec(clusters={cluster_id}))
复制代码

根据文件夹过滤虚拟机

其实和使用集群名称一样,只不过文件夹有多种类型:

  • DATACENTER = Type(string=u'DATACENTER')
  • DATASTORE = Type(string=u'DATASTORE')
  • HOST = Type(string=u'HOST')
  • NETWORK = Type(string=u'NETWORK')
  • VIRTUAL_MACHINE = Type(string=u'VIRTUAL_MACHINE')

我们在使用的时候最好精确的指定其类型。

from com.vmware.vcenter_client import Folder
# 指定文件夹类型为虚拟机文件夹,文件夹名称为 35
filter_spec = Folder.FilterSpec(type=Folder.Type.VIRTUAL_MACHINE, names={'35'})
folder_id = client.vcenter.Folder.list(filter_spec)[0].folder
client.vcenter.VM.list(client.vcenter.VM.FilterSpec(folders={folder_id}))
复制代码

还有其他的过滤方式,使用起来都一样,这里就不多提了。

从模板创建虚拟机

一些基础的用法先介绍到这,直接进入“创建虚拟机”这一正题,更多的用法也会在下面伴随着创建虚拟机一一展示。遗憾的是,我并没有在 vsphere-automation-sdk-python 中找到直接从模板中创建虚拟机的方式,所以只能退而求其次使用内容库的 ovf 模板进行部署。

在部署之前,说说这个内容库。

Content Libraries

Content Libraries(内容库)在 VSphere 6.0 中是一个容器对象,用来存储虚拟机模板、vAPP 模板、ISO 文件,还有其他你能在你网络中共享的文件。

内容库中的所有文件都可以跨 vCenter,有了内容库,你就不需要在每个 vCenter 中维护一套模板了。在 Container Libraries 中存在的虚拟机模板、vAPP 模板,还有其他文件都被定义为 library items,library items 可以包含单个或多个文件,例如 VOF、ISO 等等。

如果在多个 vCenter 之间可以进行 http/https 访问,那么这些 library items 就可以在这些 vCenter 之间共享。

在 vSphere 中可以创建两种类型的 Content Library:

  • Local:items 会被存放在某个的 vCenter 上,但是可以发布出来,让其他 vCenter 中的用户来订阅;
  • Subscribed:订阅发布出来的内容库会创建一个订阅库,订阅库可以创建在和发布库相同或不同的 vCenter 上。订阅库通过手动或者自动地同步发布库让其内容保持最新。管理员可以改变文件内容,但订阅者只能使用文件。

内容库中除了可以直接上传 ovf 文件到其中作为模板,也可以直接将 VMware 中的模板直接 copy 进去。从内容库中的模板创建机器会比直接从 VMware 上模板创建机器慢,因为它多了一个下载的过程,如果网速够快,差距会缩短。

内容库的创建方式在最下面的参考中网址中。

获取内容库中的模板

因为有了内容库的存在,我们就可以从内容库的模板中创建虚拟机了。我们要获得内容库中的内容,要先创建所谓的 stub_config,这个里面包含的是你登录 vCenter 的信息,然后将它传递给内容库相关的类,就能够获取内容库的内容了。

官方的示例文档是先创建 stub_config,我把步骤先列出来,有兴趣的人可以看看,但是这种方式明显是多此一举,不知道写示例的人咋想的。

import requests
from com.vmware.cis_client import Session
from vmware.vapi.lib.connect import get_requests_connector
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from vmware.vapi.security.session import create_session_security_context
from vmware.vapi.stdlib.client.factories import StubConfigurationFactory
from vmware.vapi.security.user_password import \
    create_user_password_security_context

server = '10.20.9.100'
username = '[email protected]'
password = '....'
skip_verification = True
host_url = f'https://{server}/api'

session = requests.Session()
session.verify = False
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
connector = get_requests_connector(session=session, url=host_url)
stub_config = StubConfigurationFactory.new_std_configuration(connector)
user_password_security_context = create_user_password_security_context(username,
                                                                           password)
stub_config.connector.set_security_context(user_password_security_context)
session_svc = Session(stub_config)
session_id = session_svc.create()
session_security_context = create_session_security_context(session_id)
stub_config.connector.set_security_context(session_security_context)
复制代码

为什么说他多此一举呢?因为一开始我们使用很简单的方式就登录了,并列出了 vCenter 中所有的虚拟机,那登陆的对象 client 中就包含了 stub_config。如果用它这种方式,你要登录两次,一次是创建 client,另一次是创建这个 stub。

简单的获取 stub_config:

import requests
import urllib3
from vmware.vapi.vsphere.client import create_vsphere_client
session = requests.session()
session.verify = False
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
client = create_vsphere_client(server='IP', username='USER', password='PASSWORD', session=session)
# 类中的变量使用 _ 开头一般是不希望在类外直接访问,但是它有这样的功能肯定直接用啊,就不管那么多了
stub_config = client._stub_config
复制代码

获得内容库 id:

from com.vmware.content_client import (Library,
                                       LibraryModel,
                                       LocalLibrary,
                                       SubscribedLibrary)

# 实例化内容库对象
library_service = Library(stub_config)
# 根据内容库的名字以及它的类型来获得内容库 id,你也可以不指定内容
# 你可以使用 list 而非 find 来列出所有的内容库 id,但是你获得的 id 并不知道它对应的名称是啥
# 因为它的返回值是字符串而非对象
library_id = library_service.find(Library.FindSpec(name='new', type=LibraryModel.LibraryType(u'LOCAL')))[0]
复制代码

根据内容库 ID 获取内容库中的模板:

from com.vmware.content.library_client import (Item,
                                               ItemModel,
                                               StorageBacking,
                                               SubscribedItem)

template_name = 'CentOS 7.5'
library_item_id = library_item_service.find(Item.FindSpec(name=template_name, library_id=library_id))[0]
library_item_obj = library_item_service.get(library_item_id)
复制代码

上面的 library_item_id 就是模板的 id,下面的 library_item_obj 则是这个模板的对象,对象中包含了该模板的一些简要信息。其实如果只是部署虚拟机的话,只需要模板 id,模板对象不重要。

创建部署目标

有了模板 id,我们还需要创建一个 DeploymentTarget,用于指定虚拟机应该部署到哪儿。它可以指定三个属性:资源池、主机和文件夹。

class LibraryItem.DeploymentTarget(resource_pool_id=None, host_id=None, folder_id=None)
复制代码

为啥没有集群呢?在没有资源池的情况下,资源池就是集群;而有资源池的情况下,也就不用指定集群了,所以资源池必须指定。剩下的主机和文件夹看需求。这三个参数的值同样不是它们的名称,而是其 id。获得资源池 id 的方法在此,主机 id 和文件夹 id 的获取方式也是如此,这里就不赘述了。

from com.vmware.vcenter.ovf_client import LibraryItem

library_item = LibraryItem(stub_config)
deployment_target = library_item.DeploymentTarget(resource_pool_id=resource_pool_id)
复制代码

创建 ResourcePoolDeploymentSpec

就如同创建虚拟机会创建一个所谓的 spec 一样,部署虚拟机同样先要创建这么个 spec,通过这个就能部署虚拟机了。这个 spec 是一个类型,支持的参数挺多:

class LibraryItem.ResourcePoolDeploymentSpec(name=None, annotation=None, accept_all_eula=None, network_mappings=None, storage_mappings=None, storage_provisioning=None, storage_profile_id=None, locale=None, flags=None, additional_parameters=None, default_datastore_id=None)
复制代码

它的所有选项针对的都是这个 ovf 模板,而不是针对通过它创建的虚拟机,这点要弄清楚。

参数说明:

  • name:str 类型或 None,虚拟机的名称;
  • annotation:str 类型或 None,虚拟机的注解信息,也是虚拟机页面的 Notes 内容;
  • accept_all_eula:bool 类型,是否接受所有的 End User License Agreements(最终用户许可协议),不知道设置为 False 能不能部署;
  • network_mappings:指定网络,这个破玩意有些复杂,它的值一个字典,字典的值是某个网络的 id,但是 key 是模板中网卡使用的网络的名称,它的获取方式在此。如果你的模板中没有网卡,那你创建的虚拟机也不存在网络;如果你模板中有网卡,但是这个参数为 None,那么部署的虚拟机将使用和模板相同的网络;
  • storage_mappings:指定存储,它的值同样是个字典,它的使用方式和网络的一致,字典的 key 获取方式和上面的网络一样,但是 key 内容就有些多了,获取方式在此。没试过将这个参数指定为 None 会怎样,应该和模板一样吧;
  • storage_provisioning:DiskProvisioningType 类型或 None,磁盘的置备类型,可以在 LibraryItem.StorageGroupMapping 中单独指定,也可以在这里全局指定;
  • storage_profile_id:str 类型或 None,看你是否使用 storage profile 了,有用的话就指定吧;
  • locale:str 类型或 None,说是用来解析 OVF 的描述的,没懂是啥意思;
  • flags:str 列表或 None,说是用来部署用的,没懂是啥意思;
  • additional_parameters:它用来指定额外的参数,有虚拟机配置的,但是没找到如何配置;有 ip 地址分配的,但是不涉及到 ip 的指定;当然还有其他一堆看不懂的属性,有需要的人可以看看,目前没有发现它的作用;
  • default_datastore_id:str 类型或 None,默认存储,没搞懂它的意思。官方说如果为 None,服务器将选择默认的存储。我就搞不懂了,这个选项不是指定默认存储的吗,怎么没指定反而使用默认存储了,难以理解。
deployment_spec = library_item.ResourcePoolDeploymentSpec(
    name="test",
    annotation="",
    accept_all_eula=True,
    network_mappings={"VM Network": network_id},
    storage_mappings={"group1": storage_group_mapping},
)
复制代码

根据网络类型的不同,network_id 的获取方式也不同,一般有标准网络和分布式网络之分。

综合下来,从内容库中创建虚拟机的方式如下:

import urllib3
import requests
from com.vmware.vcenter.ovf_client import LibraryItem, DiskProvisioningType, ImportFlag
from com.vmware.vcenter_client import Folder, ResourcePool, Cluster, Network, Datastore

from vmware.vapi.vsphere.client import create_vsphere_client
from com.vmware.content_client import (Library,
                                       LibraryModel,
                                       LocalLibrary,
                                       SubscribedLibrary)
from com.vmware.content.library_client import (Item,
                                               ItemModel,
                                               StorageBacking,
                                               SubscribedItem)

user = ""
ip = ""
password = ""
network_name = ""
cluster_name = ""
datastore_name = ""
folder_name = ""
content_library_name = ""
template_name = ""
vm_name = ""

session = requests.session()
session.verify = False
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
client = create_vsphere_client(server=ip, username=user, password=password, session=session)

stub_config = client._stub_config
library_item = LibraryItem(stub_config)
network_id = client.vcenter.Network.list(
            Network.FilterSpec(names={network_name},
                               types={Network.Type.DISTRIBUTED_PORTGROUP}))[0].network
cluster_id = client.vcenter.Cluster.list(Cluster.FilterSpec(names={cluster_name}))[0].cluster
resource_pool_id = client.vcenter.ResourcePool.list(ResourcePool.FilterSpec(clusters={cluster_id}))[
                0].resource_pool

datastore_id = client.vcenter.Datastore.list(Datastore.FilterSpec(names={datastore_name}))[0].datastore
library_service = Library(stub_config)
library_id = library_service.find(Library.FindSpec(name=content_library_name))[0]

library_item_service = Item(stub_config)
library_item_id = library_item_service.find(Item.FindSpec(name=template_name, library_id=library_id))[0]

ovf_lib_item_service = LibraryItem(stub_config)
folder_id = client.vcenter.Folder.list(Folder.FilterSpec(names={folder_name}))[0].folder
deployment_target = library_item.DeploymentTarget(resource_pool_id=resource_pool_id, folder_id=folder_id)
# ovf_lib_item_service.filter(library_item_id, deployment_target)

storage_group_mapping = ovf_lib_item_service.StorageGroupMapping(
    type=ovf_lib_item_service.StorageGroupMapping.Type('DATASTORE'),
    datastore_id=datastore_id,
    provisioning=DiskProvisioningType('thin')
)

deployment_spec = library_item.ResourcePoolDeploymentSpec(
    name=vm_name,
    annotation="",
    accept_all_eula=True,
    network_mappings={"你的配置": network_id},
    storage_mappings={"你的配置": storage_group_mapping},
)

# 执行这个语句就开始部署了,会持续 2 分钟左右,可以在 VMware 上看到具体的进度
result = library_item.deploy(library_item_id, deployment_target, deployment_spec)
复制代码

这样部署结束之后,虚拟机是创建完毕了,但是还有如下操作需要完成:

  • 更改配置
  • 添加磁盘
  • 配置 ip
  • 开机

当然这里只是列出了创建的操作,你可能还有其他需求,但是这些需求 automation sdk 都无法完成,需要使用 pyvmomi。

这也是我觉得不爽的地方,创建一个虚拟机还要使用两个 api,带来的问题就是要登录两次(一个 api 一次)。或许官方也不愿意花时间将它们整合吧。

常用操作

下面将一些常见的用法集中起来,便于查找。这一章你完全可以跳过,直接看下面的 pyvmomi。

开关机

先找到这个机器,然后查看它的状态,接着关机。

vm = client.vcenter.VM.list(client.vcenter.VM.FilterSpec(names={'10.20.6.77-xyz.tomcat.xyz-common-main.01.sit'}))[0]
client.vcenter.vm.Power.get(vm.vm)
client.vcenter.vm.Power.stop(vm.vm)
复制代码

这就关机了。除此之外,还有下面这些电源控制的方法:

  • reset:重启;
  • suspend:挂起;
  • start:启动。

判断机器已经关机:

if status == Power.Info(state=Power.State.POWERED_OFF, clean_power_off=True):
    print('VM is powered off')
复制代码

开关机可以通过 automation 或者 pyvmomi 都行,看你的兴趣了。

删除虚拟机

vm 是获取通过 list 获取到虚拟机对象之后,对象的 vm 属性。

client.vcenter.VM.delete(vm)
复制代码

删除虚拟机同样可以通过 pyvmomi。

列出集群

列出 vCenter 中所有集群:

from com.vmware.vcenter_client import Cluster

client.vcenter.Cluster.list()
复制代码

获得集群名称对应的集群 id:

cluster_name = "POC"
client_id = client.vcenter.Cluster.list(Cluster.FilterSpec(names={cluster_name}))[0].cluster
复制代码

列出文件夹

列出 vCenter 中所有文件夹:

from com.vmware.vcenter_client import Folder

client.vcenter.Folder.list()
复制代码

获取文件夹名称对应的文件夹 id:

folder_name = "35"
folder_id = client.vcenter.Folder.list(Folder.FilterSpec(names={folder_name}))[0].folder
复制代码

列出数据存储

client.vcenter.Datastore.list()
复制代码

你可能需要进行过滤:

from com.vmware.vcenter_client import Datastore

datastore_name = 'xxx'
filter_spec = Datastore.FilterSpec(names={datastore_name})
datastore_summaries = client.vcenter.Datastore.list(filter_spec)
datastore_id = datastore_summaries[0].datastore
复制代码

列出标准网络

client.vcenter.Network.list()
复制代码

过滤:

from com.vmware.vcenter_client import Network
filter = Network.FilterSpec(datacenters={datacenter},
                            names={std_porggroup_name},
                            types={Network.Type.STANDARD_PORTGROUP})
network_summaries = client.vcenter.Network.list(filter=filter)
network_id = network_summaries[0].network
复制代码

它的 type 有三种类型:

  • DISTRIBUTED_PORTGROUP:vcenter 创建和管理的网络;
  • OPAQUE_NETWORK:VSphere 之外的设备所创建,但是 vSphere 却可以知道网络的名称和标识符,所以宿主机和虚拟机的网卡才能够连接到;
  • STANDARD_PORTGROUP:ESX 创建和管理的网络。

列出分布式网络

client.vcenter.Network.list()
复制代码

过滤:

from com.vmware.vcenter_client import Network
filter = Network.FilterSpec(datacenters=set([datacenter]),
                            names=set([dv_portgroup_name]),
                            types=set([Network.Type.DISTRIBUTED_PORTGROUP]))
network_summaries = client.vcenter.Network.list(filter=filter)

if len(network_summaries) > 0:
    network_id = network_summaries[0].network
    print("Selecting Distributed Portgroup Network '{}' ({})".
            format(dv_portgroup_name, network))
else:
    print("Distributed Portgroup Network not found in Datacenter '{}'".
            format(datacenter_name))
复制代码

列出资源池

首先列出 vCenter 中所有资源池:

from com.vmware.vcenter_client import ResourcePool
client.vcenter.ResourcePool.list()
复制代码

每个集群下面必然存在一个资源池,因此我们可以通过集群名来获取其下的资源池:

cluster_name = 'XXX'
cluster_id = client.vcenter.Cluster.list(Cluster.FilterSpec(names={cluster_name}))[0].cluster
resource_pool_id = client.vcenter.ResourcePool.list(ResourcePool.FilterSpec(clusters={cluster_id}))[0].resource_pool
复制代码

创建 GuestOS

它的 api 说明在此,它其实是个封装枚举类型的类,也就是你只能选择它指定的这些值,如果新版本 api 中增加了新的值,而你要和支持新 api 的服务端通信的话,你要实例化这个类。

from com.vmware.vcenter.vm_client import GuestOS
GuestOS('CENTOS_6_64')
复制代码

创建虚拟机启动选项

官方说明在此。它用来描述如何启动一个虚拟机。

它接收的参数有:

  • type:如果为 None,GuestOS 的默认值会被使用。你自己创建的话,可以指定为 BIOS 和 EFI;
  • efi_legacy_boot:是否使用传统的 EFI 启动模式,None 的话为 GuestOS 的默认值;
  • network_protocol:网络启动所使用,可以为 IPV4 和 IPV6。如果为 None,系统的默认值被使用;
  • delay:当虚拟器开机后,延迟多少毫秒开始启动;
  • retry:当虚拟机启动失败后,是否重新启动,默认为 false;
  • retry_delay:两次重启之间的间隔,单位是毫秒,默认为 10000;
  • enter_setup_mode:指定下次虚拟机启动时候直接进去 setup 模式。一旦虚拟机进入了 setup 模式,那么它的值会置为 false;

创建 CPU

CPU 支持以下参数:

  • count:CPU 的核心数,它必须是 cores_per_socket 的倍数;
  • cores_per_socket:每插槽的 CPU 核心数,CPU 核心数必须是它的倍数;
  • hot_add_enabled:CPU 热添加是否启动,它的值只能在虚拟机关机的情况下修改;
  • hot_remove_enabled:是否启动 CPU 热减少
cpu = com.vmware.vcenter.vm.hardware_client.Cpu.UpdateSpec(count=4, cores_per_socket=2, hot_add_enabled=True, hot_remove_enabled=True)
复制代码

上面的 cpu 变量可以直接传递给 CreateSpec。

创建内存

内存能够的配置的参数如下:

  • size_mib:单位为 M;
  • hot_add_enabled:启用热添加。
GiBMemory = 1024
memory = com.vmware.vcenter.vm.hardware_client.Memory.UpdateSpec(4*GiBMemory, True)
复制代码

上面的 memory 变量可以直接传递给 CreateSpec。

创建磁盘

磁盘支持以下参数的配置:

  • type:可以为 IDE、SATA 和 SCSI,如果不指定,会使用特定于 guest 的类型;
  • ide
  • scsi
  • sata:这三个的选项都一样,都可以指定 bus(磁盘设备连接的总线号)和 unit(磁盘设备的单元号)。如果适配器上没有可用的连接,那么请求将会被拒绝;
  • backing:它和下面的 newVmdk 必须指定一个,指定它的话,你必须指定已存在的 vmdk 的路径。如果为 None,虚拟磁盘不能连接任何已存在的后端;
  • new_vmdk:为虚拟磁盘创建一个新的 vmdk backing,如果为 None,不会创建新的 vmdk 文件。如果要创建的话,可以指定容量。
from com.vmware.vcenter.vm.hardware_client import (
    Cpu, Memory, Disk, Ethernet, Cdrom, Serial, Parallel, Floppy, Boot)

GiB = 1024 * 1024 * 1024
disks=[
    Disk.CreateSpec(type=Disk.HostBusAdapterType.SCSI,
                    scsi=ScsiAddressSpec(bus=0, unit=0),
                    new_vmdk=Disk.VmdkCreateSpec(name='boot',
                                                    capacity=40 * GiB)),
    Disk.CreateSpec(new_vmdk=Disk.VmdkCreateSpec(name='data1',
                                                    capacity=10 * GiB)),
    Disk.CreateSpec(new_vmdk=Disk.VmdkCreateSpec(name='data2',
                                                    capacity=10 * GiB))
]
复制代码

创建网卡

网卡支持如下配置:

  • type:它的值是一个类实现的枚举类型,如果为 None,guest 特定类型的网卡会被创建,它的值有:
    • E1000
    • E1000E
    • PCNET32
    • VMXNET
    • VMXNET2
    • VMXNET3
  • upt_compatibility_enabled:是否兼容 Universal Pass-Through(UPT),默认是 false,不兼容;
  • mac_type:Mac 地址类型,如果为 None,使用默认值 Ethernet.MacAddressType.GENERATED;
    • ASSIGNED:Mac 地址被 vCenter 分配;
    • GENERATED:Mac 地址被自动生成;
    • MANUAL:手动配置;
  • mac_address:如果 Mac 地址类型指定为手动的话,在这里指定 Mac 地址;
  • pci_slot_number:网卡连接到哪个 PCI 总线上。如果总线地址无效,服务器将在虚拟机启动时或设备热添加时更改。如果为 None,将在虚拟机开机时自动选择一个可用的总线地址;
  • wake_on_lan_enabled:是否启用 wake-on-LAN,如果为 None 就不启用;
  • backing:虚拟网卡的物理后端。如果为 None,系统会自动寻找一个合适的后端,如果没有找到,请求失败。物理后端具有下面这些属性:
    • type:后端类型:
      • DISTRIBUTED_PORTGROUP:分布式虚拟交换机;
      • HOST_DEVICE:传统的宿主机网卡。导入的虚拟机也许有一个这种类型的网卡,但是这种类型的后端不能用来创建或升级虚拟网卡;
      • OPAQUE_NETWORK:vSphere 之外的组件所创建和管理的网络;
      • STANDARD_PORTGROUP:vSphere 标准端口组网络后端;
    • network:网络名称,这个网络名称同样是它内部维护的编号,可以在这里和这里获得。但是上面四种后端类型中,HOST_DEVICE 不支持此参数;
    • distributed_port:虚拟网卡支持的分布式虚拟端口的 key。取决于端口组的类型,端口也许使用这个字段指定。如果端口组类型是 early-binding(也称为静态),当虚拟机配置使用这个端口时,端口将自动分配,这个属性的值,将决定端口是自动还是手动分配。如果端口组类型是 ephemeral,当虚拟机开机,并且网卡连接,这个端口会自动创建并分配。这个属性不能被指定,因为在使用之前不存在空闲的端口,它也许在网络属性为 static/early-binding 时,用来指定一个端口。如果为 None,取决于端口组类型体现的策略将端口自动分配给虚拟网卡;
  • start_connected:虚拟机如果开机,网卡是否连接网络,默认为 false;
  • allow_guest_control:guest 是否能够连接或断开这个网卡;
from com.vmware.vcenter.vm.hardware_client import (
    Cpu, Memory, Disk, Ethernet, Cdrom, Serial, Parallel, Floppy, Boot)

nics = [
    Ethernet.CreateSpec(
        start_connected=True,
        mac_type=Ethernet.MacAddressType.MANUAL,
        mac_address='11:23:58:13:21:34',
        backing=Ethernet.BackingSpec(
            type=Ethernet.BackingType.STANDARD_PORTGROUP,
            network=self.standard_network)),
    Ethernet.CreateSpec(
        start_connected=True,
        mac_type=Ethernet.MacAddressType.GENERATED,
        backing=Ethernet.BackingSpec(
            type=Ethernet.BackingType.DISTRIBUTED_PORTGROUP,
            network=self.distributed_network)),
]
复制代码

获取 ovf 模板网络

通过 ovf 模板部署虚拟机,你得知道这个模板使用的网络,不然你无法指定 LibraryItem.ResourcePoolDeploymentSpec 中的 network_mappings。可能说起来有些难懂,我演示一下你就清楚了。

我们首先获得 ovf 模板的详细信息:

from com.vmware.vcenter.ovf_client import LibraryItem

# ovf_id 也就是内容库中虚拟机模板的 id,前面已经获得了,这里就不演示获取方式了
ovf_id = 'afb120c4-a173-4588-af9c-641a09a58862'

# deployment_target 前面也获取了
deployment_target = library_item.DeploymentTarget(resource_pool_id=resource_pool_id, folder_id=folder_id)
ovf_lib_item_service = LibraryItem(stub_config)
# 然后就可以获得 ovf 模板的详细信息了,这个命令的执行时间有些长
ovf_lib_item_service.filter(ovf_id, deployment_target)
复制代码

信息有些多,这里只截取部分我们想要的:

networks=['UAT53'], storage_groups=['group1']
复制代码

这里的 UAT53 就是 network_mappings 中的 key,它的 value 则是你要替换的网络 id。group1 则是 storage_mappings 的 key。

如果列表中的值有多个,相应的 network_mappings 或 storage_mappings 中的 key 就需要指定多个了。一般模板不怎么变,所以这个值第一次查出来之后,后面部署的时候就没必要查了,直接放在配置文件中就好。

指定 ovf 模板部署虚拟机的存储

api 官方说明在此。

主要还是为了创建 LibraryItem.ResourcePoolDeploymentSpec.storage_mappings 这个字典中的 value,它通过 LibraryItem.StorageGroupMapping 这个类来创建。

这个类接受如下参数:

LibraryItem.StorageGroupMapping(type=None, datastore_id=None, storage_profile_id=None, provisioning=None)
复制代码

参数说明:

  • type:官方实现的枚举类,只有两种类型,一种是 DATASTORE,另一种是 STORAGE_PROFILE。如果是 DATASTORE 类型,写起来是这样的 LibraryItem.StorageGroupMapping.Type('DATASTORE');
  • datastore_id:如果使用 DATASTORE,那么如果指定它的 id,方法在此;
  • storage_profile_id:如果使用 STORAGE_PROFILE,那么需要指定它的 id;
  • provisioning:磁盘的制备类型,有厚置备、精简置备、厚置备延迟置零三种,所以它同样是个枚举类。如果要使用精简置备,写起来是这样的 DiskProvisioningType('thin')。如果为 None,LibraryItem.ResourcePoolDeploymentSpec.storage_provisioning 将被使用。

示例:

from com.vmware.vcenter.ovf_client import LibraryItem, DiskProvisioningType

datastore_id = 'datastore-8133'
ovf_lib_item_service = LibraryItem(stub_config)
ovf_lib_item_service.StorageGroupMapping(
    type=ovf_lib_item_service.StorageGroupMapping.Type('DATASTORE'),
    datastore_id=datastore_id,
    provisioning=DiskProvisioningType('thin')
)
复制代码

pyvmomi

pyvmomi 用来给创建好的虚拟机修改配置,指定 ip 地址等操作。这样会造成一个问题,那就是创建一个虚拟机需要登录两次,一次是通过 vsphere automation sdk,还有一次就是使用 pyvmomi。因此如果只有一个 vcenter,那还是直接使用 pyvmomi 来的直接。

这里 是 pyvmomi 的使用示例,东西挺多,但是我要修改虚拟机配置和分配 ip 的操作都没有在其中找到,估计也是我没有细看的原因。

登录

先登录。

from pyVim.connect import SmartConnect, Disconnect

import atexit
import ssl
import sys

context = None
if hasattr(ssl, '_create_unverified_context'):
    context = ssl._create_unverified_context()
si = SmartConnect(host='..',
                  user='[email protected]',
                  pwd='..',
                  sslContext=context)
if not si:
    print("Could not connect to the specified host using specified "
          "username and password")
    sys.exit(1)
复制代码

列出所有主机

pyvmomi 列出主机的方式是遍历所有 vCenter 文件夹,也就是遍历 VMs and Templates 这页。先从数据中心开始,然后一级级往下。这就和 Linux 的文件系统一样,都是从根开始。

有些虚拟机没有放在文件夹下面,因此遍历的时候需要判断对象是否具有 childEntity 属性,如果有,表示它是个文件夹,可以遍历它来获得它里面的所有虚拟机。文件夹可以嵌套,因此你可能需要使用递归进行遍历了。

si.content.rootFolder.childEntity[0].vmFolder.childEntity[14].childEntity[1].name
复制代码

第一个 childEntity 表示数据中心,第二个 childEntity 表示文件夹,第三个 childEntity 表示文件夹下面的虚拟机。

查找虚拟机

虚拟机创建完成之后,我们首先得使用 pyvmomi 找到它,然后再进行操作。

from pyVim.connect import Disconnect, SmartConnectNoSSL, SmartConnect
from pyVmomi import vim, vmodl
import atexit

service_instance = SmartConnectNoSSL(host=ip, user=user, pwd=password)
atexit.register(Disconnect, service_instance)

content = service_instance.RetrieveContent()
复制代码

官方的示例中查找一个虚拟机都是将所有的虚拟机列出来,然后遍历、判断:

container = content.viewManager.CreateContainerView(content.rootFolder, [vim.VirtualMachine], True)
复制代码

但是对于 content.viewManager.CreateContainerView 的使用我们却并不清楚。我们可以在 api 文档中查找 Managed Object Types/ViewManager,可以看到 ViewManager 的类型是 vim.view.ViewManager。

反正它的第一个参数是一个文件夹类型,第二个参数则是要查看的类型,第三个是是否递归的意思。其实,我们从根目录查虚拟机有些麻烦,一是数量多,二是文件夹存在嵌套的可能,递归起来得花更多时间。反正我们创建虚拟机的时候就指定了文件夹,因此我们可以先找到这个文件夹,然后通过这个文件夹来查找虚拟机。

# 创建一个自定义异常
class TypeNotFoundException(Exception):
    def __init__(self, msg):
        self.msg = msg

def get_obj(content, folder, vimtype: list, name: str):
    obj = None
    container = content.viewManager.CreateContainerView(folder, vimtype, True)
    for c in container.view:
        if c.name == name:
            obj = c
            break
    else:
        raise TypeNotFoundException("did not find {} in {}".format(vimtype, folder))
    container.Destroy()
    return obj

folder = get_obj(content, content.rootFolder, [vim.Folder], folder_name)
vm = get_obj(content, folder, [vim.VirtualMachine], vm_name)
复制代码

vm 的 config 里面有所有该虚拟机的配置。

配置 ip 地址

虚拟机找到之后,下一步就是修改它的 ip 地址,这通过 VMware 的自定义规范来完成。但是在操作之前,确保你的虚拟机安装了 vm_tools。

CentOS 6 安装 vm_tools 可能有些麻烦,还得挂盘按照官方的要求一步步进行,但是在 CentOS 7 上你只需要安装 open-vm-tools(最小化安装中已经自带了),然后安装 perl 即可。

guest_map = vim.vm.customization.AdapterMapping()
guest_map.adapter = vim.vm.customization.IPSettings()
guest_map.adapter.ip = vim.vm.customization.FixedIp()
# 配置 ip、掩码和网关
guest_map.adapter.ip.ipAddress = ""
guest_map.adapter.subnetMask = ""
guest_map.adapter.gateway = ""


ident = vim.vm.customization.LinuxPrep()
ident.hostName = vim.vm.customization.FixedName()
# 主机名可包含字母数字字符和连字符 (-)。但不能包含句号 (.) 或空格,并且不能只由数字组成。名称不区分大小写。
ident.hostName.name = "hehe"
globalip = vim.vm.customization.GlobalIPSettings()

customspec = vim.vm.customization.Specification()
customspec.nicSettingMap = [guest_map]
customspec.identity = ident
customspec.globalIPSettings = globalip
task = vm.Customize(spec=customspec)
复制代码

执行完毕之后,通过 task 这个变量来得到是否配置成功。因为接下来很多的操作都会产生这个变量,因此可以通过下面这个函数来判断操作是否成功:

def wait_for_task(task):
    while task.info.state == "running" or task.info.state == "queued":
        time.sleep(1)

    if task.info.state == "success":
        return
    logging.error('error message')
复制代码

ip 配置完成之后会将主机名改为你指定的,且会将主机名添加到 /etc/hosts 文件中,你是你需要注意的。另外,在 CentOS 6 上,在将虚拟机作为 ovf 模板之前,最好将 /etc/udev/rules.d/70-persistent-net.rules 文件直接删除,不然你从虚拟机创建机器并为它配置上 ip 之后,可能会导致网卡名称变成 eth1。

更改虚拟机配置

这里的配置指的是 cpu 和内存:

cspec = vim.vm.ConfigSpec()
# 4 核心
cspec.numCPUs = 4
# 将 4 核心分配在两个插槽,一个插槽两核心
cspec.numCoresPerSocket = int(cspec.numCPUs / 2)
# 8G
cspec.memoryMB = 1024 * 8
# 启动 cpu 热添加
cspec.cpuHotAddEnabled = True
# 启动内存在线扩缩
cspec.memoryHotAddEnabled = True
task = vm.Reconfigure(cspec)
# 使用上面定义的函数来判断是否应用成功
wait_for_task(task)
复制代码

添加磁盘

添加磁盘也算是常见的操作了,下面的代码你直接照搬即可,这也是官方的示例。如果你要添加多块就调用多次下面的函数。

def add_disk(capacity)
    spec = vim.vm.ConfigSpec()
    dev_changes = []
    # capacity 单位为 G
    new_disk_kb = capacity * 1024 * 1024
    unit_number = 0
    # 遍历所有的硬件设备,找合适的位置添加
    for dev in vm.config.hardware.device:
        if hasattr(dev.backing, 'fileName'):
            unit_number = int(dev.unitNumber) + 1
            # unit_number 7 reserved for scsi controller
            if unit_number == 7:
                unit_number += 1
            if unit_number >= 16:
                logging.error('we don\'t support this many disks')
        if isinstance(dev, vim.vm.device.VirtualSCSIController):
            controller = dev

    disk_spec = vim.vm.device.VirtualDeviceSpec()
    disk_spec.fileOperation = "create"
    disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
    disk_spec.device = vim.vm.device.VirtualDisk()
    disk_spec.device.backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
    disk_spec.device.backing.thinProvisioned = True
    disk_spec.device.backing.diskMode = 'persistent'

    disk_spec.device.unitNumber = unit_number
    disk_spec.device.capacityInKB = new_disk_kb
    disk_spec.device.controllerKey = controller.key
    dev_changes.append(disk_spec)
    spec.deviceChange = dev_changes
    task = vm.ReconfigVM_Task(spec=spec)
    wait_for_task(task)
复制代码

一些常用的操作就弄完了,有其他的配置要求的可以去官方示例中查找。最后,配置完成后,你需要开机。开机可以使用前面提到的 automation sdk 的方式,也可以使用 pyvmomi。

自动化创建虚拟机

通过上面一套代码下来,能够完成虚拟机从无到有的过程,但是这只是将它实现了而已,如何更简单地创建才是我们需要追求的。

从上面的代码中可以看出,创建一个虚拟机需要如下属性:

  • ip
  • 主机名
  • cpu
  • 内存
  • 磁盘
  • 模板名称
  • 数据存储
  • 资源池
  • 网络
  • 文件夹

从我的经验中看,如果标准化做的好了,只要给一个主机名就可以了,其他所有属性完全可以通过主机名获得。

  • 从主机名中可以获得这个机器所属的项目,因此可以从 cmdb 的地址池中获得 ip,同时可以获得文件夹的名称(如果通过网段划分的话)。
  • 从主机名中可以获得这个机器跑的应用,因此可以获得 cpu、内存、磁盘、资源池、数据存储以及模板名称等属性。

当然,这种绝对标准化的场景很难遇到,达成起来也不容易。因此可以提供一个 rest api,将所有(或者部分)属性都通过 api 传递过来,然后进行创建。

我的个人建议是模板就是刚最小化安装的系统,不要有任何多余的操作。在创建虚拟机完毕之后,需要做的操作都可以通过初始化脚本完成。我的做法是每创建完一台机器,等待开机时间之后使用 ansible 连上去进行初始化操作。

我会为每台机器启动一个初始化线程,这个线程调用 ansible-playbook 命令(没必要使用 ansible api),只为这一台机器进行初始化。这样只要判断 ansible-playbook 命令的返回值就能够判断该虚拟机是否初始化成功。

创建虚拟机和初始化要分为两个步骤,不然你初始化失败了,重试的话难道要从创建机器开始吗?不同的步骤重试方式自然是不一样的。

创建机器

创建机器只是作为对 automation sdk 的了解,并不实用,因为我们会从模板生成机器,而并不直接创建。

创建机器需要指定诸如集群、数据存储、文件夹、数据中心等属性,然后根据这些属性来创建一个 placement。从字面意思来看,placement 是一个放置虚拟机的属性,指明虚拟机应该放在哪。

创建 placement 需要上述所说的属性,就拿数据存储来说,你不能直接提供给它数据存储的名字,它不认这个,想必有过前面的经验你也知道了,它要的是它内部维护的所谓的编号这样的东西,这个东西通过数据存储的名字来获得。

比如我先根据数据存储的名字来获得这个数据存储的对象:

from com.vmware.vcenter_client import Datastore
filter_spec = Datastore.FilterSpec(names={datastore_name})
datastores = client.vcenter.Datastore.list(filter_spec)
复制代码

很显然,最终的结果是一个列表。如果列表长度为 0,那表示没有这个数据存储。假如你给的名称正确的话,它的长度为 1,那数据存储的编号就是这个:

datastores[0].datastore
复制代码

其他属性也一样,只不过集群为 VAR[0].cluster,文件夹为 VAR[0].folder。

创建 placement

前面也提到了,Placement 的作用就是指定虚拟机创建在哪,因此它接收的参数有:

  • folder:文件夹
  • resource_pool:资源池
  • host:宿主机。如果资源池和宿主机同时指定了,你必须得保证资源池属于这个宿主机;如果宿主机和集群也都同时执行,你也必须保证宿主机属于集群;
  • cluster:集群。如果集群和资源池同时指定,资源池必须属于集群;
  • datastore:数据存储

通过这些参数就可以创建一个 placement 对象。

创建 CreateSpec

接下来要给出所有创建一个虚拟机的属性了,它们共同构成 CreateSpec,最后才根据 CreateSpec 创建虚拟机。我们之前已经获得 placement 了,接下来还可以获得标准交换机或分布式交换机。有了这些之后,就可以尝试着创建 CreateSpec 了。

它接收的参数特别多:

  • guest_os:指定操作系统类型,创建的方式在此;
  • name:指定虚拟机的名称,如果为空(None),服务器会自动为你生成;
  • placement:之前已经创建了,就不多提了。官方表示当前版本还必须要指定,未来服务端自动帮你选择一个合适的地方放虚拟机;
  • hardware_version:为空(None)就好,服务端会帮你选择最新的;
  • boot:启动选项,如果为 None,guest_os 默认值会被使用。它的使用方式在此;
  • boot_devices:从什么设备启动系统,一般都为系统硬盘,它的值是个列表。如果为 None,一个应用于服务器的启动列表被使用;
  • cpu:CPU 相关的配置,它的创建方式在此。如果为 None,使用系统默认值;
  • memory:内存相关的配置,它是创建方式在此。如果为 None,使用系统默认值;
  • disks:磁盘配置,它的是值是一个列表,单个磁盘的创建方式在此。如果为 None,单个 guest 特定的大小的空虚拟磁盘被创建在虚拟机配置的相同存储上;
  • nics:网卡配置,它的值是一个列表,单个网卡的创建方式在此。如果为 None,不会创建任何网卡;
  • cdroms
  • floppies
  • parallel_ports
  • serial_ports
  • sata_adapters:SATA 适配器列表,为 None 让它自动创建吧;
  • scsi_adapters:同上。

贴一个官方示例吧:

guest_os = testbed.config['VM_GUESTOS']
iso_datastore_path = testbed.config['ISO_DATASTORE_PATH']
serial_port_network_location = \
    testbed.config['SERIAL_PORT_NETWORK_SERVER_LOCATION']

GiB = 1024 * 1024 * 1024
GiBMemory = 1024

vm_create_spec = VM.CreateSpec(
    guest_os=guest_os,
    name=self.vm_name,
    placement=self.placement_spec,
    hardware_version=Hardware.Version.VMX_11,
    cpu=Cpu.UpdateSpec(count=2,
                        cores_per_socket=1,
                        hot_add_enabled=False,
                        hot_remove_enabled=False),
    memory=Memory.UpdateSpec(size_mib=2 * GiBMemory,
                                hot_add_enabled=False),
    disks=[
        Disk.CreateSpec(type=Disk.HostBusAdapterType.SCSI,
                        scsi=ScsiAddressSpec(bus=0, unit=0),
                        new_vmdk=Disk.VmdkCreateSpec(name='boot',
                                                        capacity=40 * GiB)),
        Disk.CreateSpec(new_vmdk=Disk.VmdkCreateSpec(name='data1',
                                                        capacity=10 * GiB)),
        Disk.CreateSpec(new_vmdk=Disk.VmdkCreateSpec(name='data2',
                                                        capacity=10 * GiB))
    ],
    nics=[
        Ethernet.CreateSpec(
            start_connected=True,
            mac_type=Ethernet.MacAddressType.MANUAL,
            mac_address='11:23:58:13:21:34',
            backing=Ethernet.BackingSpec(
                type=Ethernet.BackingType.STANDARD_PORTGROUP,
                network=self.standard_network)),
        Ethernet.CreateSpec(
            start_connected=True,
            mac_type=Ethernet.MacAddressType.GENERATED,
            backing=Ethernet.BackingSpec(
                type=Ethernet.BackingType.DISTRIBUTED_PORTGROUP,
                network=self.distributed_network)),
    ],
    cdroms=[
        Cdrom.CreateSpec(
            start_connected=True,
            backing=Cdrom.BackingSpec(type=Cdrom.BackingType.ISO_FILE,
                                        iso_file=iso_datastore_path)
        )
    ],
    serial_ports=[
        Serial.CreateSpec(
            start_connected=False,
            backing=Serial.BackingSpec(
                type=Serial.BackingType.NETWORK_SERVER,
                network_location=serial_port_network_location)
        )
    ],
    parallel_ports=[
        Parallel.CreateSpec(
            start_connected=False,
            backing=Parallel.BackingSpec(
                type=Parallel.BackingType.HOST_DEVICE)
        )
    ],
    floppies=[
        Floppy.CreateSpec(
            backing=Floppy.BackingSpec(
                type=Floppy.BackingType.CLIENT_DEVICE)
        )
    ],
    boot=Boot.CreateSpec(type=Boot.Type.BIOS,
                            delay=0,
                            enter_setup_mode=False
                            ),
    # TODO Should DISK be put before CDROM and ETHERNET?  Does the BIOS
    # automatically try the next device if the DISK is empty?
    boot_devices=[
        BootDevice.EntryCreateSpec(BootDevice.Type.CDROM),
        BootDevice.EntryCreateSpec(BootDevice.Type.DISK),
        BootDevice.EntryCreateSpec(BootDevice.Type.ETHERNET)
    ]
)
复制代码

创建虚拟机

有了 createSpec 就可以直接创建虚拟机了:

vm = self.client.vcenter.VM.create(vm_create_spec)
复制代码

报错

使用 vsphere automation sdk 连接 vcenter 的时候会报下面的错:

/usr/local/python3/bin/python3 /root/PycharmProjects/demo/zabbix/tmp.py
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/urllib3/contrib/pyopenssl.py", line 453, in wrap_socket
    cnx.do_handshake()
  File "/usr/local/python3/lib/python3.6/site-packages/OpenSSL/SSL.py", line 1907, in do_handshake
    self._raise_ssl_error(self._ssl, result)
  File "/usr/local/python3/lib/python3.6/site-packages/OpenSSL/SSL.py", line 1632, in _raise_ssl_error
    raise SysCallError(-1, "Unexpected EOF")
OpenSSL.SSL.SysCallError: (-1, 'Unexpected EOF')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/urllib3/connectionpool.py", line 594, in urlopen
    self._prepare_proxy(conn)
  File "/usr/local/python3/lib/python3.6/site-packages/urllib3/connectionpool.py", line 805, in _prepare_proxy
    conn.connect()
  File "/usr/local/python3/lib/python3.6/site-packages/urllib3/connection.py", line 344, in connect
    ssl_context=context)
  File "/usr/local/python3/lib/python3.6/site-packages/urllib3/util/ssl_.py", line 357, in ssl_wrap_socket
    return context.wrap_socket(sock)
  File "/usr/local/python3/lib/python3.6/site-packages/urllib3/contrib/pyopenssl.py", line 459, in wrap_socket
    raise ssl.SSLError('bad handshake: %r' % e)
ssl.SSLError: ("bad handshake: SysCallError(-1, 'Unexpected EOF')",)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/requests/adapters.py", line 449, in send
    timeout=timeout
  File "/usr/local/python3/lib/python3.6/site-packages/urllib3/connectionpool.py", line 638, in urlopen
    _stacktrace=sys.exc_info()[2])
  File "/usr/local/python3/lib/python3.6/site-packages/urllib3/util/retry.py", line 398, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='', port=443): Max retries exceeded with url: /api (Caused by SSLError(SSLError("bad handshake: SysCallError(-1, 'Unexpected EOF')",),))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/root/PycharmProjects/demo/zabbix/tmp.py", line 52, in 
    client = create_vsphere_client(server=ip, username=user, password=password, session=session)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/vsphere/client.py", line 170, in create_vsphere_client
    hok_token=hok_token, private_key=private_key)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/vsphere/client.py", line 111, in __init__
    session_id = session_svc.create()
  File "/usr/local/python3/lib/python3.6/site-packages/com/vmware/cis_client.py", line 197, in create
    return self._invoke('create', None)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/bindings/stub.py", line 317, in _invoke
    return self._api_interface.native_invoke(ctx, _method_name, kwargs)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/bindings/stub.py", line 243, in native_invoke
    method_result = self.invoke(ctx, method_id, data_val)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/bindings/stub.py", line 179, in invoke
    ctx)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/security/client/security_context_filter.py", line 99, in invoke
    self, service_id, operation_id, input_value, new_ctx)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/provider/filter.py", line 76, in invoke
    service_id, operation_id, input_value, ctx)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/protocol/client/msg/json_connector.py", line 79, in invoke
    response = self._do_request(VAPI_INVOKE, params)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/protocol/client/msg/json_connector.py", line 120, in _do_request
    headers=request_headers, body=request_body))
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/protocol/client/rpc/requests_provider.py", line 98, in do_request
    cookies=http_request.cookies, timeout=timeout)
  File "/usr/local/python3/lib/python3.6/site-packages/requests/sessions.py", line 533, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/local/python3/lib/python3.6/site-packages/requests/sessions.py", line 646, in send
    r = adapter.send(request, **kwargs)
  File "/usr/local/python3/lib/python3.6/site-packages/requests/adapters.py", line 514, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: HTTPSConnectionPool(host='', port=443): Max retries exceeded with url: /api (Caused by SSLError(SSLError("bad handshake: SysCallError(-1, 'Unexpected EOF')",),))
Exception ignored in: 0x7f17d4c42908>>
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/vsphere/client.py", line 136, in __del__
    if hasattr(self, 'session'):
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/bindings/stub.py", line 415, in __getattr__
    return getattr(self._stub_factory, name)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/bindings/stub.py", line 415, in __getattr__
    return getattr(self._stub_factory, name)
  File "/usr/local/python3/lib/python3.6/site-packages/vmware/vapi/bindings/stub.py", line 415, in __getattr__
    return getattr(self._stub_factory, name)
  [Previous line repeated 328 more times]
RecursionError: maximum recursion depth exceeded while calling a Python object
复制代码

搜一个下,发现这样一个回答,它的意思是 vcenter 只支持 3DES 作为 cipher,而 requests 却将其从默认的 cipher 列表中将其剔除了,因为它默认并不安全。

但是我使用 openssl 命令查看了一把,发现并不是这个原因,因为 vcenter 使用了 tls1.2,而并非 SSLv1/v2/v3。

# openssl s_client -connect IP:443
...
No client certificate CA names sent
Peer signing digest: SHA512
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 1418 bytes and written 415 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    Session-ID:
    Session-ID-ctx:
    Master-Key: E3C9A906DA95855044E8DC4D512084A98A605C3B538505956568585C3555FF371E35F8FE05043734EC73E5D59346B3FC
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1552027935
    Timeout   : 300 (sec)
    Verify return code: 21 (unable to verify the first certificate)
---
read:errno=0
复制代码

最后通过抓包发现,原来是因为我系统使用了代理,https 经过代理貌似就会出问题。不知道这是不是 Python 的问题,当 pip 使用代理来安装的时候,也会报 https 相关的错误。其他语言写的软件就没有这样的问题。

参考

www.starwindsoftware.com/blog/workin…

转载于:https://juejin.im/post/5d08c5cee51d45777a1261a2

你可能感兴趣的:(使用 vsphere-automation-sdk-python 自动创建虚拟机)