VMware Python api

想自动分 VMware 虚拟机,因此抽空研究了下它的 Python api。VMware api 本质上是 restapi,只不过有多种语言的封装,若是有兴趣能够关注下 VMware 其余语言的实现。html

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。python

简单来讲,之后新的功能的实现就在 vsphere automation sdk 上。虽然如此,可是 vsphere automation sdk 未必就必定适合你。并且 vsphere automation sdk 无论你用仍是不用,pyvmomi 是必定要使用的git

为何这么说呢?vsphere-automation-sdk-python 因为使用了新的 api,可使用相似于内容库(关于内容库的内容,接下来会提到)以及 tagging 这样的新功能,可是其余功能就乏善可陈了,好比它对虚拟机自己的管理就没有丝毫办法。github

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

所以,在存在多个 vcenter 的环境,可使用内容库保持多个 vcenter 之间 ovf 模板的一致,这样无论在哪一个 vcenter 中建立虚拟机操做都同样。而后建立完虚拟机以后,再使用 pyvmomi 对虚拟机配置进行修改。固然,若是你的环境中只有一个 vcenter,那么彻底就用不到 vsphere-automation-sdk,由于使用 pyvmomi 直接克隆机器速度更快,由于它少了从内容库下载模板的时间。vim

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

咱们能够先进入其 github 主页api

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

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

本文基于 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 = 'administrator@vsphere.local'
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,
    # 这个必定要有,不然不是精简置备
    storage_provisioning=DiskProvisioningType('thin'),
    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='administrator@vsphere.local',
                  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 <module>
    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: <bound method VsphereClient.__del__ of <vmware.vapi.vsphere.client.VsphereClient object at 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…

相关文章
相关标签/搜索