Help us understand the problem. What is going on with this article?

Gophercloud を用いた OpenStack 操作

More than 3 years have passed since last update.

前置き

gophercloud とは、OpenStack プロジェクトを生み出した Rackspace 社が提供する公式の golang の SDK で、複数のクラウドプロバイダで共通の WebAPI を叩けるようになるというもの(と言っても今扱えるプロバイダは Rackspace と OpenStack しかなさそう)。
https://developer.rackspace.com/sdks/golang/

golang を使って上手く OpenStack を操作できないかな〜と思ってたところ、こういうものがあると知って一通り触ってみたので、どういう風に使うのかをまとめてみました。確かめた OpenStack のバージョンは Liberty だと思います(間違ってたら訂正します)。

参考資料

準備

gophercloud の API を使用するために、OpenStack の認証に必要な情報を環境変数に読み込んでおく必要があります(以下で説明しますが、OS_AUTH_URLOS_TOKEN など)。

また、認証には不要ですが、各サービスの API を叩くためのエンドポイントのリージョン指定や、LBaaS 作成時にテナントの指定が必要なので、以下の環境変数も適宜設定(または、コード内で直接指定)する必要があります。
ただし、自ユーザーが所属する以外のテナントを指定することができるのは admin 権限を持つユーザーだけみたい。

  • OS_REGION_NAME : リージョン名
  • OS_TENANT_ID : テナントID
  • OS_TENANT_NAME : テナント名

認証方法

以下は、環境変数から認証に必要な情報をオプション (opts) として読み込み、そのオプションを元に認証するだけのサンプルコードを示します (pc は ProviderClient の略)。この処理を実施した以降は API が叩けるようになります。

認証するために必要な環境変数の設定は以下3パターンです。どのパターンでも認証することが可能です。

  • パターン1
    • OS_AUTH_URL: 認証を行うURL、かつ
    • OS_TOKEN : トークン
  • パターン2
    • OS_AUTH_URL: 認証を行うURL、かつ
    • OS_USERNAME : ユーザー名、かつ
    • OS_PASSWORD : パスワード
  • パターン3
    • OS_AUTH_URL: 認証を行うURL、かつ
    • OS_USERID : ユーザーID、かつ
    • OS_PASSWORD : パスワード

gophercloud.AuthOptions 構造体のメンバとの対応関係は以下の通りです。直接 AuthOptions 構造体を作成し、メンバに値を埋め込んでオプション情報を作成することはできますが、シークレット情報 (パスワードやToken) はシークレットストアを経由して渡すようにしましょう。

メンバ変数 環境変数
IdentityEndpoint OS_AUTH_URL
UserID OS_USERID
Username OS_USERNAME
Password OS_PASSWORD
auth.go
package main

import (
    "fmt"
    "github.com/rackspace/gophercloud/openstack"
)

func main() {
    opts, err := openstack.AuthOptionsFromEnv()
    if err != nil {
        fmt.Println(err)
        return
    }

    pc, err := openstack.AuthenticatedClient(opts)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(pc)
}

flavor に関する操作

API 仕様は以下URLです (WebAPI のドキュメントはこちら) 。
https://godoc.org/github.com/rackspace/gophercloud/openstack/compute/v2/flavors

基本的に認証済みの ProviderClient を用いて ServiceClient (sc) を作成し、それに対して WebAPI が発行されるイメージとなります。

ListDetail()

以下に flavor のリストを出力するサンプルコードを示します。

list_flavors.go
import "os"
import "github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
import "github.com/rackspace/gophercloud/pagination"

...(省略)...

sc, err := openstack.NewComputeV2(pc, gophercloud.EndpointOpts{
    Region: os.Getenv("OS_REGION_NAME"),
})
if err != nil {
    fmt.Println(err)
    return
}

pager := flavors.ListDetail(sc, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
    flavorList, err := flavors.ExtractFlavors(page)
    if err != nil {
        fmt.Println(err)
        return false, err
    }
    for _, f := range flavorList {
        fmt.Println(f.ID, f.Name)
    }
    return true, nil
})

Get()

flavor の名前からIDを引いて Flavor 構造体を GET するなら以下の通り。

get_flavor.go
flavor_name := "m1.medium"
flavor_id, err := flavors.IDFromName(sc, flavor_name)
if err != nil {
    fmt.Println(err)
    return
}

f1, err := flavors.Get(sc, flavor_id).Extract()
if err != nil {
    fmt.Println(err)
    return
}

image に関する操作

OpenStack の Glance に該当する部分ですね。

API 仕様は以下URLです (WebAPI のドキュメントはこちら) 。ただ、members は登録したイメージに対してアクセスできるメンバーを細かく管理するためのもので、細かく image の管理をしたいという用途以外あまり必要そうでないので、今回は割愛させていただきます。
https://godoc.org/github.com/rackspace/gophercloud/openstack/imageservice/v2/images
https://godoc.org/github.com/rackspace/gophercloud/openstack/imageservice/v2/members

gophercloud/openstack/compute/v2/images については、Compute API のドキュメントを確認すると DEPRECATED となっているので使うべきではないでしょう。

List()

以下に、image のリストを出力するサンプルコードを示します。ServiceClient (sc) は imageservice 用の API (NewImageServiceV2) から取得します。

list_images.go
import "github.com/rackspace/gophercloud/openstack/imageservice/v2/images"
import "github.com/rackspace/gophercloud/pagination"

...(省略)...

sc, err := openstack.NewImageServiceV2(pc, gophercloud.EndpointOpts{
    Region: os.Getenv("OS_REGION_NAME"),
})
if err != nil {
    fmt.Println(err)
    return
}

listopts := images.ListOpts{}
pager := images.List(sc, listopts)
pager.EachPage(func(page pagination.Page) (bool, error) {
    imageList, err := images.ExtractImages(page)
    if err != nil {
        fmt.Println(err)
        return false, err
    }
    for _, i := range imageList {
        fmt.Println(i)
    }
    return true, nil
})

Create(), Upload()

新しいイメージを作成してアップロードするサンプルは以下の通りです。アップロード元のイメージは、例えば CoreOS を使用するならばこういうところを参考にして事前にダウンロード・解凍しておくとよいでしょう。

upload_image.go
import "time"

...(省略)...

copts := images.CreateOpts{
    Name:            "CoreOS-1068.8.0-Stable",
    ContainerFormat: "bare",
    DiskFormat:      "qcow2",
}
image, err := images.Create(sc, copts).Extract()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(image)
id := image.ID

file, err := os.Open("./coreos_production_openstack_image.img")
if err != nil {
    fmt.Println(err)
    return
}
defer file.Close()

fmt.Println("Start Uploading image")
go func() {
    images.Upload(sc, id, file)
}()

start := time.Now()
for {
    img, err := images.Get(sc, id).Extract()
    if err != nil {
        fmt.Println(err)
        return
    }
    if img.Status == "active" {
        fmt.Println("Uploading image is completed!")
        break
    }
    fmt.Println("... waiting")
    time.Sleep(5 * time.Second)
    if time.Now().Sub(start) > (3 * time.Minute) {
        fmt.Println("Uploading image is not completed in 3 minutes.")
        break
    }
}

Delete()

イメージ削除のサンプルは以下の通りです。List() で削除したいイメージにマッチするものを検索して Delete() するだけの処理となっていますが、本当に削除して良いか確認する処理を入れた方が良いと思います。

delete_image.go
listopts := images.ListOpts{Name: "CoreOS-1068.8.0-Stable"}
pager := images.List(sc, listopts)
pager.EachPage(func(page pagination.Page) (bool, error) {
    imageList, err := images.ExtractImages(page)
    if err != nil {
        fmt.Println(err)
        return false, err
    }
    for _, i := range imageList {
        fmt.Println(i.ID, i.Name)
        result := images.Delete(sc, i.ID)
        if result.Err != nil {
            fmt.Println(err)
            return false, err
        }
        fmt.Println("Deleting image is completed!")
    }
    return true, nil
})

server (VM) に関する操作

こちらは主に Nova に対応する部分です。API 仕様は以下URLです (WebAPI のドキュメントはこちら) 。
https://godoc.org/github.com/rackspace/gophercloud/openstack/compute/v2/servers

List()

以下に、server のリストを出力するサンプルコードを示します。ServiceClient (sc) は compute 用の API (NewComputeV2) から取得します。

list_servers.go
import "os"
import "github.com/rackspace/gophercloud/openstack/compute/v2/servers"
import "github.com/rackspace/gophercloud/pagination"

・・・(省略)・・・

sc, err := openstack.NewComputeV2(pc, gophercloud.EndpointOpts{
    Region: os.Getenv("OS_REGION_NAME"),
})
if err != nil {
    fmt.Println(err)
    return
}

pager := servers.List(sc, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
    serverList, err := servers.ExtractServers(page)
    if err != nil {
        fmt.Println(err)
        return false, err
    }
    for _, s := range serverList {
        fmt.Println(s.ID, s.Name, s.Status)
    }
    return true, nil
})

Create(), Delete()

server の作成例は以下の通りです。
terraform コマンドを使ってマシンを作成するのと同様に、flavor名、OSイメージ名などを指定して作成します。この例ではキーペアを作成済みである場合に server 作成を中止していますが、通常は既存のキーペアをそのまま用いると思います。また、キーペア作成時の秘密鍵をファイル test-private-key に出力していますが、これは例えば Vault などのシークレットストアに保存して使用する時だけ取り出すようにした方が良いでしょう。

server 作成時のキーペアの指定は optional であり、servers.CreateOpts には含まれていない extension のフィールドであることに注意してください。keypairs.CreateOptsExt 構造体の KeyName にキーペア名を入れる必要があります。

userdata については、yaml 形式のファイル sample_userdata.yml の中身を []byte 型にして読み込むことで処理しています。userdata は、API が POST を実行する際に Base64 でエンコードされます。

server のステータスの種類と各 action に対するステータスの遷移についてはここを確認してください。以下の例では、ステータスが ACTIVE になるまで待ち合わせていますが、他の条件も合わせて待ちたいならば gophercloud.WaitFor() を用いて、自分で待ち合わせ条件を作ることもできます。

create_server.go
import "io/ioutil"
import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs"

・・・(省略)・・・

kopts := keypairs.CreateOpts{
    Name: "test-key",
}
stored_kp, _ := keypairs.Get(sc, kopts.Name).Extract()
if stored_kp != nil {
    fmt.Println("Already created keypair:", kopts.Name)
    return
}

kp, err := keypairs.Create(sc, kopts).Extract()
if err != nil {
    fmt.Println(err)
    return
}

err = ioutil.WriteFile("./test-private-key", []byte(kp.PrivateKey), 0400)
if err != nil {
    fmt.Println(err)
    return
}

userdata, err := ioutil.ReadFile("./sample_userdata.yml")
if err != nil {
    fmt.Println(err)
    return
}
copts := servers.CreateOpts{
    Name:           "test.server.sandbox.pluto",
    ImageName:      "CoreOS-899.17.0-Stable",
    FlavorName:     "m1.medium",
    SecurityGroups: []string{"all"},
    UserData:       userdata,
    Networks:       []servers.Network{{UUID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}},
}

coptsext := keypairs.CreateOptsExt{
    copts,
    kp.Name,
}

server, err := servers.Create(sc, coptsext).Extract()
if err != nil {
    fmt.Println(err)
    return
}
sid := server.ID
err = servers.WaitForStatus(sc, sid, "ACTIVE", 60)
if err != nil {
    fmt.Println(err)
    return
}
server, err = servers.Get(sc, sid).Extract()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(server)

server の削除は、作成した server の ID を servers.Delete() の引数として入れるだけです。返り値として DeleteResult 型が返されるので、ExtractErr() を使えばエラーの中身を確認できます。

Start(), Stop()

以下に、server を起動するサンプルコードを示します。存在する server のIDを取得して、起動をかけます。停止処理をする場合は、startstop.Start()startstop.Stop() に変更するだけです。
server の action 操作で起動・停止だけ compute/v2/extensions/startstop のパッケージに分かれているので注意してください。

start_server.go
import  "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop"

・・・(省略)・・・

sid, err := servers.IDFromName(sc, "test-server")
if err != nil {
    fmt.Println(err)
    return
}

fmt.Println("Start machine:", sid)
err = startstop.Start(sc, sid).ExtractErr()
if err != nil {
    fmt.Println(err)
    return
}

その他の action

Reboot()

ソフトリブートするかハードリブートするかは Reboot() の第三引数に RebootMethod により指定可能です。以下の例ではソフトリブート servers.OSReboot を指定しています。

reboot_server.go
sid, err := servers.IDFromName(sc, "test.server")
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

fmt.Println("Reboot server:", sid)
err = servers.Reboot(sc, sid, servers.OSReboot).ExtractErr()
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

err = servers.WaitForStatus(sc, sid, "ACTIVE", 300)
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

server, err := servers.Get(sc, sid).Extract()
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

fmt.Println("server:", server)

Resize()

servers.Resize() でリサイズ出来るはずだと思ってましたが、API を発行したところHTTP 403 が返ってきました。以下のような感じです。

Expected HTTP response code [201 202] when accessing [POST http://省略], but got 403 instead
{"forbidden": {"message": "Policy doesn't allow os_compute_api:servers:resize to be performed.", "code": 403}}

自分の環境上で Horizon からインスタンスのリサイズを実行したところ、「リサイズを予約しました」と表示されるも実際はリサイズされない結果となったので、gophercloud でやっても同じ結果となるのは当然かなと思います。まぁ、できるようになったらまたトライしてみようかな。

ForceDelete()

ある server に対してどんな操作をしてもエラーになるなど、どうしようもなくなった時に強制削除を使用するケースがあると思いますので、そういう時はこれを使いましょう。

force_delete_server.go
sid, err := servers.IDFromName(sc, "test.server")
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

err = servers.ForceDelete(sc, sid).ExtractErr()
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

baremetal に関する操作

WebAPI のドキュメントはこちらですが、これらの API は Mitaka 5.1.0 から追加されたようです。

もし、ベアメタルを扱っていても、認証先のURLをベアメタルを扱っているところに向ければ、Nova (compute) と同じ API でも操作可能なようです。分かり次第、また追記したいと思います。

LBaaS に関する操作

ここは、Neutron に該当する部分ですね。API 仕様は以下URLです (WebAPI のドキュメントはこちら) 。
https://godoc.org/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2

以下では、LB として Octavia を主に使用しています。

Loadbalancer

List()

以下に、loadbalancer のリストを出力するサンプルコードを示します。ServiceClient (sc) は network 用の API (NewNetworkV2) から取得します。

list_lb.go
import "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers"

・・・(省略)・・・

sc, err := openstack.NewNetworkV2(pc, gophercloud.EndpointOpts{
    Region: os.Getenv("OS_REGION_NAME"),
})
if err != nil {
    fmt.Println(err)
    return
}

pager := loadbalancers.List(sc, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
    lbList, err := loadbalancers.ExtractLoadbalancers(page)
    if err != nil {
        fmt.Println(err)
        return false, err
    }
    for _, lb := range lbList {
        fmt.Println(lb)
    }
    return true, nil
})

Create(), Delete()

以下に、loadbalancer を作成するサンプルコードを示します。loadbalancer には WaitForStatus() が存在しないので、gophercloud.WaitFor() を使用してステータスが ACTIVE になるのを監視しています(おそらく、LoadBalancer 型に ProvisioningStatusOperatingStatus の2種類のステータスがあるため、わざと作ってないように思います)。

create_lb.go
copts := loadbalancers.CreateOpts{
    Name:         "test.lb.gophercloud",
    VipSubnetID:  "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    TenantID:     os.Getenv("OS_TENANT_ID"),
    AdminStateUp: loadbalancers.Up,
    Provider:     "octavia",
}
loadbalancer, err := loadbalancers.Create(sc, copts).Extract()
if err != nil {
    fmt.Println(err)
    return
}

lbID := loadbalancer.ID
gophercloud.WaitFor(120, func() (bool, error) {
    lb, err := loadbalancers.Get(sc, lbID).Extract()
    if err != nil {
        return false, err
    }
    if lb.ProvisioningStatus != "ACTIVE" {
        return false, nil
    }
    return true, nil
})

lb1, err := loadbalancers.Get(sc, lbID).Extract()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println("loadbalancer:", lb1)

loadbalancer の削除は、作成した loadbalancer の ID を loadbalancers.Delete() の引数として入れるだけです。返り値として DeleteResult 型が返されるので、ExtractErr() を使えばエラーの中身を確認できます。

Listener

List()

以下に、listener のリストを出力するサンプルコードを示します。

list_listener.go
import "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
import "github.com/rackspace/gophercloud/pagination"

・・・(省略)・・・

lbID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
lopts := listeners.ListOpts{
    LoadbalancerID: lbID,
}
pager := listeners.List(sc, lopts)
pager.EachPage(func(page pagination.Page) (bool, error) {
    listenerList, err := listeners.ExtractListeners(page)
    if err != nil {
        fmt.Println(err)
        return false, err
    }
    for _, l := range listenerList {
        fmt.Println(l)
    }
    return true, nil
})

Create(), Delete()

以下に、listener を作成するサンプルコードを示します。作成時は loadbalancer のステータスを確認しておく方が良いと思います。

create_listener.go
import  "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers"

・・・(省略)・・・

lbID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
http_copts := listeners.CreateOpts{
    Protocol:       listeners.ProtocolHTTP,
    ProtocolPort:   80,
    TenantID:       os.Getenv("OS_TENANT_ID"),
    LoadbalancerID: lbID,
    Name:           "test.listener.http",
}
http_listener, err := listeners.Create(sc, http_copts).Extract()
if err != nil {
    fmt.Println(err)
    return
}

gophercloud.WaitFor(120, func() (bool, error) {
    lb, err := loadbalancers.Get(sc, lbID).Extract()
    if err != nil {
        return false, err
    }
    if lb.ProvisioningStatus != "ACTIVE" {
        return false, nil
    }
    return true, nil
})

https_copts := listeners.CreateOpts{
    Protocol:       listeners.ProtocolHTTPS,
    ProtocolPort:   443,
    TenantID:       os.Getenv("OS_TENANT_ID"),
    LoadbalancerID: lbID,
    Name:           "test.listener.https",
}
https_listener, err := listeners.Create(sc, https_copts).Extract()
if err != nil {
    fmt.Println(err)
    return
}

gophercloud.WaitFor(120, func() (bool, error) {
    lb, err := loadbalancers.Get(sc, lbID).Extract()
    if err != nil {
        return false, err
    }
    if lb.ProvisioningStatus != "ACTIVE" {
        return false, nil
    }
    return true, nil
})

listener の削除は、作成した listener の ID を listeners.Delete() の引数として入れるだけです。返り値として DeleteResult 型が返されるので、ExtractErr() を使えばエラーの中身を確認できます。

Pool

List()

以下に、pool のリストを出力するサンプルコードを示します。

list_pool.go
import "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
import "github.com/rackspace/gophercloud/pagination"

・・・(省略)・・・

lbID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
lopts := pools.ListOpts{
    LoadbalancerID: lbID,
}
pager := pools.List(sc, lopts)
pager.EachPage(func(page pagination.Page) (bool, error) {
    poolList, err := pools.ExtractPools(page)
    if err != nil {
        fmt.Println(err)
        return false, err
    }
    for _, p := range poolList {
        fmt.Println(p)
    }
    return true, nil
})

Create(), Delete()

以下に、pool を作成するサンプルコードを示します。作成時は loadbalancer のステータスを確認しておく方が良いと思います(以下の例では、listener や loadbalancer の ID (listenerID, lbID) は仮の値を入れてます)。

create_pool.go
import "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers"

・・・(省略)・・・

listenerID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
copts := pools.CreateOpts{
    LBMethod:   pools.LBMethodRoundRobin,
    Protocol:   pools.ProtocolHTTPS,
    ListenerID: listenerID,
    Name:       "test.pool.https",
}
pool, err := pools.Create(sc, copts).Extract()
if err != nil {
    fmt.Println(err)
    return
}

lbID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
gophercloud.WaitFor(120, func() (bool, error) {
    lb, err := loadbalancers.Get(sc, lbID).Extract()
    if err != nil {
        return false, err
    }
    if lb.ProvisioningStatus != "ACTIVE" {
        return false, nil
    }
    return true, nil
})

p1, err := pools.Get(sc, pool.ID).Extract()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(p1)

pool の削除は、作成した pool の ID を pools.Delete() の引数として入れるだけです。返り値として DeleteResult 型が返されるので、ExtractErr() を使えばエラーの中身を確認できます。

CreateAssociateMember(), DeleteMember()

作成した pool に member を追加するサンプルコードを以下に示します。

ソースを見ると、SubnetID を指定しない場合は loadbalancer の vip_subnet_id を使用すると記載されてますが、実際に指定しない場合はエラーが返ってきました ({"NeutronError": {"message": "Failed to parse request. Required attribute 'subnet_id' not specified", "type": "HTTPBadRequest", "detail": ""}})。そのため、使用するサブネットの ID は指定しないとダメみたいですね。

create_member.go
poolID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
mcopts := pools.MemberCreateOpts{
    Address:      "192.168.20.12",
    ProtocolPort: 80,
    SubnetID:     "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
}

member, err := pools.CreateAssociateMember(sc, poolID, mcopts).Extract()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(member)

member の削除は、作成した member の ID と追加した pool の ID を pools.DeleteMember() の引数として入れるだけです。返り値として DeleteResult 型が返されるので、ExtractErr() を使えばエラーの中身を確認できます。

ブロックストレージに関する操作

ここは、Cinder に該当する処理です。API 仕様は以下URLです (WebAPI のドキュメントはこちら) 。
https://godoc.org/github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes
https://godoc.org/github.com/rackspace/gophercloud/openstack/blockstorage/v2/extensions/volumeactions

ただし、作成した volume を server にアタッチ・デタッチする操作は volumeattach パッケージを使用してください。現状、volumeactions パッケージでは正しく処理が完了しないようです1 2

Block Storage API v1 の方は DEPRECATED になっていますので、v1 系の API は使わない方が良いでしょう。

List()

以下に、volume のリストを出力するサンプルコードを示します。ServiceClient (sc) は volume 用の API (NewBlockStrageV2) から取得します。

list_volume.go
import "github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes"
import "github.com/rackspace/gophercloud/pagination"

・・・(省略)・・・

sc, err := openstack.NewBlockStorageV2(sp, gophercloud.EndpointOpts{
    Region: os.Getenv("OS_REGION_NAME"),
})
if err != nil {
    fmt.Println(err)
    return
}

pager := volumes.List(sc, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
    volumeList, err := volumes.ExtractVolumes(page)
    if err != nil {
        fmt.Println(err)
        return false, err
    }
    for _, v := range volumeList {
        fmt.Println(v)
    }
    return true, nil
})

Create(), Delete()

以下に、volume を作成するサンプルコードを示します。例では作成した volume のステータスが利用可能状態 (available) になるまで10秒待ち合わせる処理を入れてます。ステータスの種類はここで確認してください。ステータスは文字列で扱うことに注意ください。

create_volume.go
copts := volumes.CreateOpts{
    Description: "Test for gophercloud",
    Name:        "test.volume",
    Size:        5,
    VolumeType:  "NetappSATA",
}
volume, err := volumes.Create(sc, copts).Extract()
if err != nil {
    fmt.Println(err)
    return
}

err = volumes.WaitForStatus(sc, volume.ID, "available", 10)
if err != nil {
    fmt.Println(err)
    return
}

volume, err = volumes.Get(sc, volume.ID).Extract()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(volume)

volume の削除は、作成した volume の ID を volumes.Delete() の引数として入れるだけです。返り値として DeleteResult 型が返されるので、ExtractErr() を使えばエラーの中身を確認できます。

volumeattach.Create(), volumeattach.Delete()

以下に、volume を attach/detach するサンプルコードを示します (volume と server の ID は仮の値としているので、そのまま使用しないでください)。ここでは volumeattach パッケージを使用するので、ServiceClient (sc) は NewComputeV2 で取得しています。

volumeattach.Create() 時に返り値として受け取った VolumeAttachment 型の ID メンバ の値は (現時点は) VolumeID と同じ値が入っているようです。

attach_detach_volume.go
import "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"

・・・(省略)・・・

sc, err := openstack.NewComputeV2(pc, gophercloud.EndpointOpts{
    Region: os.Getenv("OS_REGION_NAME"),
})
if err != nil {
    fmt.Println(err)
    return
}

volumeID := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
serverID := "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

// Attach volume
copts := volumeattach.CreateOpts{
    Device:   "/dev/vdb",
    VolumeID: volumeID,
}
volat, err := volumeattach.Create(sc, serverID, copts).Extract()
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(volat)

// Detach volume
err = volumeattach.Delete(sc, serverId, volat.ID).ExtractErr()
if err != nil {
    fmt.Println(err)
    return
}

  1. volumeactions パッケージを使用してみましたが、アタッチの API を叩いてみると volume のステータスはアタッチされた状態になっているのに、server のステータスは何もアタッチしていない状態のままになっていました。リクエストの body や POST する URL は WebAPI ドキュメント通りであることは確認したので、gophercloud には問題ないと思います。おそらく、現状の OpenStack 側に問題があり、バージョンアップで対応される可能性があります。 

  2. volumeactions パッケージでは AttachOpts 型でアタッチ先のサーバを指定することができ、アタッチ先が VM であればインスタンス ID を AttachOpts.InstanceUUID に指定、アタッチ先がベアメタルであればホスト名を AttachOpts.HostName に指定する仕様になっています (これらの指定は排他)。今後このパッケージを使えるようになれば、Cinder で作成した volume をベアメタルにアタッチする処理も可能になってくるのではと考えられます。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away