gophercloud简介
gophercloud是OpenStack的Golang SDK包,第三方应用程序可以通过这个包提供的API接口调用到OpenStack云相关的API服务。
gophercloud的流程
既然是通过API调用服务,那就和使用OpenStack API的程序处于同一位置,大致可以分为如下几个步骤。
- 提供鉴权的地址,用户名&密码(或者是token),domain信息
- 提供Cert 文件,Key文件
- 通过鉴权地址建立安全连接后,发送1中的信息进行鉴权。
- 鉴权成功后获得token和其它服务的地址(服务目录)
- 通过服务的地址和token访问其它的服务。
这部分的核心代码参见这里
gophercloud具体实现——两个client + 1个factory
既然是要通过code调用API服务,那么肯定需要封装能访问这些API服务的client,在gophercloud中一共有两个client,分别代表了两个层次的client(严格上说是3个,这里有没有把http client这一层算作是gophercloud的client)。这两个client分别是service-client和provider-client。大致关系如下:
其中provider-client是service-client的一个generic implement,它是所有服务访问的基础client,provider-client主要是封装了http-client,构建http消息通过http-client发送,然后再处理返回消息以及异常。它的结构体声明如下:
// ProviderClient stores details that are required to interact with any
// services within a specific provider's API.
//
// Generally, you acquire a ProviderClient by calling the NewClient method in
// the appropriate provider's child package, providing whatever authentication
// credentials are required.
type ProviderClient struct {
// IdentityBase is the base URL used for a particular provider's identity
// service - it will be used when issuing authenticatation requests. It
// should point to the root resource of the identity service, not a specific
// identity version.
IdentityBase string
// IdentityEndpoint is the identity endpoint. This may be a specific version
// of the identity service. If this is the case, this endpoint is used rather
// than querying versions first.
IdentityEndpoint string
// TokenID is the ID of the most recently issued valid token.
TokenID string
// EndpointLocator describes how this provider discovers the endpoints for
// its constituent services.
EndpointLocator EndpointLocator
// HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
HTTPClient http.Client
// UserAgent represents the User-Agent header in the HTTP request.
UserAgent UserAgent
// ReauthFunc is the function used to re-authenticate the user if the request
// fails with a 401 HTTP response code. This a needed because there may be multiple
// authentication functions for different Identity service versions.
ReauthFunc func() error
Debug bool
}
它的核心成员是HTTPClient,核心方法是Request方法,申明如下:
// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
// header will automatically be provided.
func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error)
Request方法通过对http client封装出提供对远端http服务的访问方法,该方法主要有三个参数,method,url,和options,method是指http请求的类型,如get,post,put,delete等,url是指访问远端的http资源路径,后面会简单介绍它的生成规则,options则是访问该远程服务提供的一些参数。
基于provider-client,会衍生出各种服务client,如:计算服务client,存储服务client,鉴权服务client等等,这些服务clients并不是每一种服务定义了一个结构体,而是抽象成了一个结构体申明,那就是service-client. 这里先介绍下service-client的内部,至于如果构建这些不同类型的服务client的,稍后介绍。
service-client继承至provider-client,但是对provider-client进行了具体化,把provider的request请求装饰成了rest请求类型:get, put, post, delete, patch,并对provider的属性针对各个服务API进行了进一步定义,具体结构定义如下:
// ServiceClient stores details required to interact with a specific service API implemented by a provider.
// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient.
type ServiceClient struct {
// ProviderClient is a reference to the provider that implements this service.
*ProviderClient
// Endpoint is the base URL of the service's API, acquired from a service catalog.
// It MUST end with a /.
Endpoint string
// ResourceBase is the base URL shared by the resources within a service's API. It should include
// the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used
// as-is, instead.
ResourceBase string
// This is the service client type (e.g. compute, sharev2).
// NOTE: FOR INTERNAL USE ONLY. DO NOT SET. GOPHERCLOUD WILL SET THIS.
// It is only exported because it gets set in a different package.
Type string
// The microversion of the service to use. Set this to use a particular microversion.
Microversion string
}
这里的核心就是provider-client,所有的业务处理都转换成provider-client的request进行发送和接收。
回到刚才的话题,针对不同的服务会创建出不同的service-client,那么service-client这些属性如何设置?如果不做封装使用起来将会是噩梦,gophercloud当然不会让这个噩梦出现,于是提供相应的工厂方法——client,这里的client是一个提供构建各种服务实例(service-client实例)的工厂方法的集合,在这个client里可以找到openstack已有的所有服务client的构建方法,如:计算服务、网络服务、存储服务等等。
// NewComputeV2 creates a ServiceClient that may be used with the v2 compute
// package.
func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "compute")
}
// NewNetworkV2 creates a ServiceClient that may be used with the v2 network
// package.
func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "network")
sc.ResourceBase = sc.Endpoint + "v2.0/"
return sc, err
}
// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1
// block storage service.
func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "volume")
}
在构建这些service-client时需要转入provider-client和EndpointOpts(后面介绍这个参数),其中provider-client也可以通过client工厂进行构建,见下:
A basic example of using this would be:
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.NewClient(ao.IdentityEndpoint)
client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{})
*/
func NewClient(endpoint string) (*gophercloud.ProviderClient, error)
通过上述这些工厂构建出相应的service-client后,利用service-client的get,post,put,delete等方法,再加上相应的参数,就可以调用OpenStack的API了,具体每个OpenStack API的参数,请参见这里,gophercloud针对各个服务需要的参数也定义了相应的结构体,分别在https://github.com/freesky-edward/gophercloud/tree/master/openstack 的子目录下。如创建块存储所的需要的参数定义在CreateOpts里,并且封装了相应的方法,如创建块存储的方法定义如下,详细参见这里
// Create will create a new Volume based on the values in CreateOpts. To extract
// the Volume object from the response, call the Extract method on the
// CreateResult.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToVolumeCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
return
}
这个方法的核心就是调用service-client的Post方法,只是由于rest的uri是/v3/{project_id}/volumes需要指定是volume操作,所以需要通过createURL(client)构建该URI,具体构建规则后面会介绍.
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("volumes")
}
这样所有的对外接口就全呈现出来了,对于gophercloud的使用者来讲,首先通过client.NewClient构建一个provider-client,然后利用这个provider-client和EndpointOpts通过各个服务工厂方法(如块存储服务工厂client.NewBlockStorageV1)构建出service-client。最后使用这个service-client加上相应的调用参数就可以调用相应的服务接口了,如创建块存储func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder)。
前面在介绍创建相应的service-client时,留有一个问题——EndpointOpts是啥?
在介绍EndpointOpts之前,就上述的主流程介绍中有一个细节需要再详细说明一下,在OpenStack里每个API服务都有自己endpoint,这个endpoint注册在keystone服务里,通过admin查看keystone的服务目录,大致结果如下图:
在构建service-client时需要用到这个endpoint地址,在每次向OpenStack API发送rest请求时,需要根据这个endpoint来构建rest的uri(前面有提到),最后将这个uri传给provider-client的request进行远端服务调用,那么如果创建这个uri的呢?
uri是基于上图中的endpoint加上子路径构成,如创建存储的uri是http://10.229.47.230/volume/v3/slob/volumes就是前面图中的endpoint+volumes构成的,要构建出这个uri,首先需要获得这个endpoint,要获得这个endpoint就需要上面提到的EndpointOpts,一般在构建完成provider-client后,通过调用client工厂的Authenticate方法进行鉴权,鉴权成功后,就可以获取到当前用户可以访问的API目录(服务目录),以V3版本为例,详细代码见这里
v3Client, err := NewIdentityV3(client, eo)
if err != nil {
return err
}
if endpoint != "" {
v3Client.Endpoint = endpoint
}
result := tokens3.Create(v3Client, opts)
token, err := result.ExtractToken()
if err != nil {
return err
}
catalog, err := result.ExtractServiceCatalog()
if err != nil {
return err
}
client.TokenID = token.ID
if opts.CanReauth() {
client.ReauthFunc = func() error {
client.TokenID = ""
return v3auth(client, endpoint, opts, eo)
}
}
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return V3EndpointURL(catalog, opts)
}
catalog, err := result.ExtractServiceCatalog()就是解析出相应的服务目录,获得服务目录后需要通过EndpointOpts来定义的参数进行过滤获得具体的服务endpoint,这里主要是根据region,服务提供的范围(上图中的Interface)进行过滤,详细代码见这里:
for _, entry := range catalog.Entries {
if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
for _, endpoint := range entry.Endpoints {
if opts.Availability != gophercloud.AvailabilityAdmin &&
opts.Availability != gophercloud.AvailabilityPublic &&
opts.Availability != gophercloud.AvailabilityInternal {
err := &ErrInvalidAvailabilityProvided{}
err.Argument = "Availability"
err.Value = opts.Availability
return "", err
}
if (opts.Availability == gophercloud.Availability(endpoint.Interface)) &&
(opts.Region == "" || endpoint.Region == opts.Region) {
endpoints = append(endpoints, endpoint)
}
}
}
}
而这个过滤方法是在鉴权后保存在provider-client的EndpointLocator里,在client的工厂创建各个service-client时会调用这个方法得到endpoint,并将其注入到service-client的Endpoint里,代码请参见这里:
func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) {
sc := new(gophercloud.ServiceClient)
eo.ApplyDefaults(clientType)
url, err := client.EndpointLocator(eo)
if err != nil {
return sc, err
}
sc.ProviderClient = client
sc.Endpoint = url
sc.Type = clientType
return sc, nil
}
service-cliet在进行具体的服务调用时,会根据这里的endpoint来拼装uri(前面介绍的createURL),具体拼装逻辑封装在service-client 的ServiceURL方法里:
// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /.
func (client *ServiceClient) ResourceBaseURL() string {
if client.ResourceBase != "" {
return client.ResourceBase
}
return client.Endpoint
}
// ServiceURL constructs a URL for a resource belonging to this provider.
func (client *ServiceClient) ServiceURL(parts ...string) string {
return client.ResourceBaseURL() + strings.Join(parts, "/")
}
所以EndpointOpts就是需要指定这个client是用于哪个region、那种服务范围(调用内部或者外部)的API服务的一个结构体,技术就是用来选择endpoint的。
总结
对于gophercloud来讲,看似代码量很大,但是只要我们理解了它的核心工作职责,抓住了它的骨架——两个client+1个工厂后,剩下的逻辑就非常简单了,都是针对不同服务去定义相应的参数和返回处理,以及请求URL的拼装。