Gophercloud を用いた OpenStack 操作

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

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


# 参考資料
- [OpenStack の WebAPI ドキュメント: OpenStack API Complete Reference](http://developer.openstack.org/api-ref.html)
    - どの API が DEPRECATED になったか定期的に確認した方が良さそう。
- [gophercloud のホームページ](http://gophercloud.io/)
    - 中身が古くなっている箇所や間違い箇所も多々見受けられるので、あまり役に立たない。
- [gophercloud パッケージの API ドキュメント](https://godoc.org/github.com/rackspace/gophercloud)
    - 下位ディレクトリを辿っていくと、各クラウドプロバイダの API 仕様を確認可能。
- [gophercloud のソース](https://github.com/rackspace/gophercloud)
    - 最新情報を確認する時はここ。


# 準備
gophercloud の API を使用するために、OpenStack の認証に必要な情報を環境変数に読み込んでおく必要があります（以下で説明しますが、`OS_AUTH_URL` や `OS_TOKEN` など）。

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

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

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

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

- パターン１
    - `OS_AUTH_URL`: 認証を行うURL、かつ
    - `OS_TOKEN` : トークン
- パターン２
    - `OS_AUTH_URL`: 認証を行うURL、かつ
    - `OS_USERNAME` : ユーザー名、かつ
    - `OS_PASSWORD` : パスワード
- パターン３
    - `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 のドキュメントは[こちら](http://developer.openstack.org/api-ref/compute/#flavors)) 。
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 のドキュメントは[こちら](http://developer.openstack.org/api-ref-image-v2.html)) 。ただ、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](https://godoc.org/github.com/rackspace/gophercloud/openstack/compute/v2/images) については、[Compute API のドキュメント](http://developer.openstack.org/api-ref/compute/?expanded=create-image-createimage-action-detail#images-deprecated)を確認すると 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 を使用するならば[こういうところを参考](https://coreos.com/os/docs/latest/booting-on-openstack.html)にして事前にダウンロード・解凍しておくとよいでしょう。

```: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 のドキュメントは[こちら](http://developer.openstack.org/api-ref/compute/#servers-servers)) 。
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` コマンド](https://www.terraform.io/)を使ってマシンを作成するのと同様に、flavor名、OSイメージ名などを指定して作成します。この例ではキーペアを作成済みである場合に server 作成を中止していますが、通常は既存のキーペアをそのまま用いると思います。また、キーペア作成時の秘密鍵をファイル `test-private-key` に出力していますが、これは例えば [Vault](https://www.hashicorp.com/vault.html) などのシークレットストアに保存して使用する時だけ取り出すようにした方が良いでしょう。

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

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

[server のステータスの種類と各 action に対するステータスの遷移についてはここを確認してください。](http://docs.openstack.org/developer/nova/vmstates.html#virtual-machine-states-and-transitions)以下の例では、ステータスが 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 のドキュメントは[こちら](http://developer.openstack.org/api-ref/baremetal/index.html)ですが、これらの API は Mitaka 5.1.0 から追加されたようです。

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


# LBaaS に関する操作
ここは、Neutron に該当する部分ですね。API 仕様は以下URLです (WebAPI のドキュメントは[こちら](http://developer.openstack.org/api-ref/networking/v2-ext/)) 。
https://godoc.org/github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2

以下では、LB として [Octavia](https://wiki.openstack.org/wiki/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` 型に `ProvisioningStatus` と `OperatingStatus` の２種類のステータスがあるため、わざと作ってないように思います）。

```: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 のドキュメントは[こちら](http://developer.openstack.org/api-ref-blockstorage-v2.html)) 。
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 パッケージ](http://developer.openstack.org/api-ref/compute/#servers-with-volume-attachments-servers-os-volume-attachments)を使用してください。現状、volumeactions パッケージでは正しく処理が完了しないようです[^1] [^2]。

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

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

[Block Storage API v1](http://developer.openstack.org/api-ref-blockstorage-v1.html) の方は DEPRECATED になっていますので、[v1 系の API](https://godoc.org/github.com/rackspace/gophercloud/openstack/blockstorage/v1) は使わない方が良いでしょう。

## `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秒待ち合わせる処理を入れてます。[ステータスの種類はここで確認してください](http://developer.openstack.org/api-ref-blockstorage-v2.html#volumes-v2-volumes)。ステータスは文字列で扱うことに注意ください。

```: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 メンバ](https://godoc.org/github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach#VolumeAttachment) の値は （現時点は） 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
}
```