Add distributed routers support

This commit is contained in:
Kirill Shirinkin 2016-02-12 18:57:41 +01:00
parent fe90cea9a1
commit 63016155ea
15 changed files with 190 additions and 25 deletions

4
Godeps/Godeps.json generated
View File

@ -655,8 +655,8 @@
}, },
{ {
"ImportPath": "github.com/rackspace/gophercloud", "ImportPath": "github.com/rackspace/gophercloud",
"Comment": "v1.0.0-774-g680aa02", "Comment": "v1.0.0-803-g6769c3b",
"Rev": "680aa02616313d8399abc91f17a444cf9292f0e1" "Rev": "6769c3b3e54a5cf1b0bdb10ea5b25f5cff0a3134"
}, },
{ {
"ImportPath": "github.com/satori/go.uuid", "ImportPath": "github.com/satori/go.uuid",

View File

@ -37,6 +37,12 @@ func resourceNetworkingRouterV2() *schema.Resource {
ForceNew: false, ForceNew: false,
Computed: true, Computed: true,
}, },
"distributed": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Computed: true,
},
"external_gateway": &schema.Schema{ "external_gateway": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
@ -69,6 +75,11 @@ func resourceNetworkingRouterV2Create(d *schema.ResourceData, meta interface{})
createOpts.AdminStateUp = &asu createOpts.AdminStateUp = &asu
} }
if dRaw, ok := d.GetOk("distributed"); ok {
d := dRaw.(bool)
createOpts.Distributed = &d
}
externalGateway := d.Get("external_gateway").(string) externalGateway := d.Get("external_gateway").(string)
if externalGateway != "" { if externalGateway != "" {
gatewayInfo := routers.GatewayInfo{ gatewayInfo := routers.GatewayInfo{
@ -126,6 +137,7 @@ func resourceNetworkingRouterV2Read(d *schema.ResourceData, meta interface{}) er
d.Set("name", n.Name) d.Set("name", n.Name)
d.Set("admin_state_up", n.AdminStateUp) d.Set("admin_state_up", n.AdminStateUp)
d.Set("distributed", n.Distributed)
d.Set("tenant_id", n.TenantID) d.Set("tenant_id", n.TenantID)
d.Set("external_gateway", n.GatewayInfo.NetworkID) d.Set("external_gateway", n.GatewayInfo.NetworkID)

View File

@ -91,10 +91,12 @@ var testAccNetworkingV2Router_basic = fmt.Sprintf(`
resource "openstack_networking_router_v2" "foo" { resource "openstack_networking_router_v2" "foo" {
name = "router" name = "router"
admin_state_up = "true" admin_state_up = "true"
distributed = "false"
}`) }`)
var testAccNetworkingV2Router_update = fmt.Sprintf(` var testAccNetworkingV2Router_update = fmt.Sprintf(`
resource "openstack_networking_router_v2" "foo" { resource "openstack_networking_router_v2" "foo" {
name = "router_2" name = "router_2"
admin_state_up = "true" admin_state_up = "true"
distributed = "false"
}`) }`)

View File

@ -3,6 +3,7 @@ package openstack
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"strings"
"github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud"
tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens" tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
@ -64,8 +65,8 @@ func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.Provider
// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint. // Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint.
func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
versions := []*utils.Version{ versions := []*utils.Version{
&utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"}, {ID: v20, Priority: 20, Suffix: "/v2.0/"},
&utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"}, {ID: v30, Priority: 30, Suffix: "/v3/"},
} }
chosen, endpoint, err := utils.ChooseVersion(client, versions) chosen, endpoint, err := utils.ChooseVersion(client, versions)
@ -110,7 +111,7 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
if options.AllowReauth { if options.AllowReauth {
client.ReauthFunc = func() error { client.ReauthFunc = func() error {
client.TokenID = "" client.TokenID = ""
return AuthenticateV2(client, options) return v2auth(client, endpoint, options)
} }
} }
client.TokenID = token.ID client.TokenID = token.ID
@ -168,7 +169,7 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
if options.AllowReauth { if options.AllowReauth {
client.ReauthFunc = func() error { client.ReauthFunc = func() error {
client.TokenID = "" client.TokenID = ""
return AuthenticateV3(client, options) return v3auth(client, endpoint, options)
} }
} }
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
@ -198,6 +199,40 @@ func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClien
} }
} }
func NewIdentityAdminV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("identity")
eo.Availability = gophercloud.AvailabilityAdmin
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
// Force using v2 API
if strings.Contains(url, "/v3") {
url = strings.Replace(url, "/v3", "/v2.0", -1)
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
func NewIdentityAdminV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("identity")
eo.Availability = gophercloud.AvailabilityAdmin
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
// Force using v3 API
if strings.Contains(url, "/v2.0") {
url = strings.Replace(url, "/v2.0", "/v3", -1)
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package. // NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package.
func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("object-store") eo.ApplyDefaults("object-store")

View File

@ -15,6 +15,7 @@ const (
Volume SourceType = "volume" Volume SourceType = "volume"
Snapshot SourceType = "snapshot" Snapshot SourceType = "snapshot"
Image SourceType = "image" Image SourceType = "image"
Blank SourceType = "blank"
) )
// BlockDevice is a structure with options for booting a server instance // BlockDevice is a structure with options for booting a server instance
@ -32,6 +33,9 @@ type BlockDevice struct {
// and "local". // and "local".
DestinationType string `json:"destination_type"` DestinationType string `json:"destination_type"`
// GuestFormat [optional] specifies the format of the block device.
GuestFormat string `json:"guest_format"`
// SourceType [required] must be one of: "volume", "snapshot", "image". // SourceType [required] must be one of: "volume", "snapshot", "image".
SourceType SourceType `json:"source_type"` SourceType SourceType `json:"source_type"`
@ -82,6 +86,9 @@ func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
if bd.DestinationType != "" { if bd.DestinationType != "" {
blockDevice[i]["destination_type"] = bd.DestinationType blockDevice[i]["destination_type"] = bd.DestinationType
} }
if bd.GuestFormat != "" {
blockDevice[i]["guest_format"] = bd.GuestFormat
}
} }
serverMap["block_device_mapping_v2"] = blockDevice serverMap["block_device_mapping_v2"] = blockDevice

View File

@ -399,6 +399,18 @@ func HandleServerDeletionSuccessfully(t *testing.T) {
}) })
} }
// HandleAdminPasswordChangeSuccessfully sets up the test server to respond to a server password
// change request.
func HandleServerForceDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `{ "forceDelete": "" }`)
w.WriteHeader(http.StatusAccepted)
})
}
// HandleServerGetSuccessfully sets up the test server to respond to a server Get request. // HandleServerGetSuccessfully sets up the test server to respond to a server Get request.
func HandleServerGetSuccessfully(t *testing.T) { func HandleServerGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) { th.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {

View File

@ -303,6 +303,17 @@ func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
return res return res
} }
func ForceDelete(client *gophercloud.ServiceClient, id string) ActionResult {
var req struct {
ForceDelete string `json:"forceDelete"`
}
var res ActionResult
_, res.Err = client.Post(actionURL(client, id), req, nil, nil)
return res
}
// Get requests details on a single server, by ID. // Get requests details on a single server, by ID.
func Get(client *gophercloud.ServiceClient, id string) GetResult { func Get(client *gophercloud.ServiceClient, id string) GetResult {
var result GetResult var result GetResult

View File

@ -15,9 +15,9 @@ type Scope struct {
} }
func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string { func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
h := c.AuthenticatedHeaders() return map[string]string{
h["X-Subject-Token"] = subjectToken "X-Subject-Token": subjectToken,
return h }
} }
// Create authenticates and either generates a new token, or changes the Scope of an existing token. // Create authenticates and either generates a new token, or changes the Scope of an existing token.

View File

@ -16,6 +16,7 @@ type ListOpts struct {
ID string `q:"id"` ID string `q:"id"`
Name string `q:"name"` Name string `q:"name"`
AdminStateUp *bool `q:"admin_state_up"` AdminStateUp *bool `q:"admin_state_up"`
Distributed *bool `q:"distributed"`
Status string `q:"status"` Status string `q:"status"`
TenantID string `q:"tenant_id"` TenantID string `q:"tenant_id"`
Limit int `q:"limit"` Limit int `q:"limit"`
@ -46,6 +47,7 @@ func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
type CreateOpts struct { type CreateOpts struct {
Name string Name string
AdminStateUp *bool AdminStateUp *bool
Distributed *bool
TenantID string TenantID string
GatewayInfo *GatewayInfo GatewayInfo *GatewayInfo
} }
@ -62,6 +64,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
type router struct { type router struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
AdminStateUp *bool `json:"admin_state_up,omitempty"` AdminStateUp *bool `json:"admin_state_up,omitempty"`
Distributed *bool `json:"distributed,omitempty"`
TenantID *string `json:"tenant_id,omitempty"` TenantID *string `json:"tenant_id,omitempty"`
GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
} }
@ -73,6 +76,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
reqBody := request{Router: router{ reqBody := request{Router: router{
Name: gophercloud.MaybeString(opts.Name), Name: gophercloud.MaybeString(opts.Name),
AdminStateUp: opts.AdminStateUp, AdminStateUp: opts.AdminStateUp,
Distributed: opts.Distributed,
TenantID: gophercloud.MaybeString(opts.TenantID), TenantID: gophercloud.MaybeString(opts.TenantID),
}} }}
@ -96,6 +100,7 @@ func Get(c *gophercloud.ServiceClient, id string) GetResult {
type UpdateOpts struct { type UpdateOpts struct {
Name string Name string
AdminStateUp *bool AdminStateUp *bool
Distributed *bool
GatewayInfo *GatewayInfo GatewayInfo *GatewayInfo
Routes []Route Routes []Route
} }
@ -109,6 +114,7 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResu
type router struct { type router struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
AdminStateUp *bool `json:"admin_state_up,omitempty"` AdminStateUp *bool `json:"admin_state_up,omitempty"`
Distributed *bool `json:"distributed,omitempty"`
GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
Routes []Route `json:"routes"` Routes []Route `json:"routes"`
} }
@ -120,6 +126,7 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResu
reqBody := request{Router: router{ reqBody := request{Router: router{
Name: gophercloud.MaybeString(opts.Name), Name: gophercloud.MaybeString(opts.Name),
AdminStateUp: opts.AdminStateUp, AdminStateUp: opts.AdminStateUp,
Distributed: opts.Distributed,
}} }}
if opts.GatewayInfo != nil { if opts.GatewayInfo != nil {

View File

@ -35,6 +35,9 @@ type Router struct {
// Administrative state of the router. // Administrative state of the router.
AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
// Whether router is disitrubted or not..
Distributed bool `json:"distributed" mapstructure:"distributed"`
// Human readable name for the router. Does not have to be unique. // Human readable name for the router. Does not have to be unique.
Name string `json:"name" mapstructure:"name"` Name string `json:"name" mapstructure:"name"`

View File

@ -104,6 +104,7 @@ type CreateOpts struct {
DeviceOwner string DeviceOwner string
TenantID string TenantID string
SecurityGroups []string SecurityGroups []string
AllowedAddressPairs []AddressPair
} }
// ToPortCreateMap casts a CreateOpts struct to a map. // ToPortCreateMap casts a CreateOpts struct to a map.
@ -139,6 +140,9 @@ func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
if opts.MACAddress != "" { if opts.MACAddress != "" {
p["mac_address"] = opts.MACAddress p["mac_address"] = opts.MACAddress
} }
if opts.AllowedAddressPairs != nil {
p["allowed_address_pairs"] = opts.AllowedAddressPairs
}
return map[string]interface{}{"port": p}, nil return map[string]interface{}{"port": p}, nil
} }
@ -174,6 +178,7 @@ type UpdateOpts struct {
DeviceID string DeviceID string
DeviceOwner string DeviceOwner string
SecurityGroups []string SecurityGroups []string
AllowedAddressPairs []AddressPair
} }
// ToPortUpdateMap casts an UpdateOpts struct to a map. // ToPortUpdateMap casts an UpdateOpts struct to a map.
@ -198,6 +203,9 @@ func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
if opts.Name != "" { if opts.Name != "" {
p["name"] = opts.Name p["name"] = opts.Name
} }
if opts.AllowedAddressPairs != nil {
p["allowed_address_pairs"] = opts.AllowedAddressPairs
}
return map[string]interface{}{"port": p}, nil return map[string]interface{}{"port": p}, nil
} }

View File

@ -19,7 +19,6 @@ func (r commonResult) Extract() (*Port, error) {
var res struct { var res struct {
Port *Port `json:"port"` Port *Port `json:"port"`
} }
err := mapstructure.Decode(r.Body, &res) err := mapstructure.Decode(r.Body, &res)
return res.Port, err return res.Port, err
@ -51,6 +50,11 @@ type IP struct {
IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"` IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"`
} }
type AddressPair struct {
IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"`
MACAddress string `mapstructure:"mac_address" json:"mac_address,omitempty"`
}
// Port represents a Neutron port. See package documentation for a top-level // Port represents a Neutron port. See package documentation for a top-level
// description of what this is. // description of what this is.
type Port struct { type Port struct {
@ -78,6 +82,8 @@ type Port struct {
SecurityGroups []string `mapstructure:"security_groups" json:"security_groups"` SecurityGroups []string `mapstructure:"security_groups" json:"security_groups"`
// Identifies the device (e.g., virtual server) using this port. // Identifies the device (e.g., virtual server) using this port.
DeviceID string `mapstructure:"device_id" json:"device_id"` DeviceID string `mapstructure:"device_id" json:"device_id"`
// Identifies the list of IP addresses the port will recognize/accept
AllowedAddressPairs []AddressPair `mapstructure:"allowed_address_pairs" json:"allowed_address_pairs"`
} }
// PortPage is the page returned by a pager when traversing over a collection // PortPage is the page returned by a pager when traversing over a collection

View File

@ -177,6 +177,9 @@ func (client *ProviderClient) Request(method, url string, options RequestOpts) (
} }
} }
// Set connection parameter to close the connection immediately when we've got the response
req.Close = true
// Issue the request. // Issue the request.
resp, err := client.HTTPClient.Do(req) resp, err := client.HTTPClient.Do(req)
if err != nil { if err != nil {
@ -246,6 +249,8 @@ func defaultOkCodes(method string) []int {
return []int{201, 202} return []int{201, 202}
case method == "PUT": case method == "PUT":
return []int{201, 202} return []int{201, 202}
case method == "PATCH":
return []int{200, 204}
case method == "DELETE": case method == "DELETE":
return []int{202, 204} return []int{202, 204}
} }
@ -299,6 +304,24 @@ func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse
return client.Request("PUT", url, *opts) return client.Request("PUT", url, *opts)
} }
func (client *ProviderClient) Patch(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
}
return client.Request("PATCH", url, *opts)
}
func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) {
if opts == nil { if opts == nil {
opts = &RequestOpts{} opts = &RequestOpts{}

View File

@ -249,3 +249,38 @@ func ListEvents(client *gophercloud.ServiceClient, loadBalancerID int, opts List
return NodeEventPage{pagination.SinglePageBase(r)} return NodeEventPage{pagination.SinglePageBase(r)}
}) })
} }
// GetByIPPort locates a load balancer node by IP and port.
func GetByIPPort(
client *gophercloud.ServiceClient,
loadBalancerID int,
address string,
port int,
) (*Node, error) {
// nil until found
var found *Node
List(client, loadBalancerID, nil).EachPage(func(page pagination.Page) (bool, error) {
lbNodes, err := ExtractNodes(page)
if err != nil {
return false, err
}
for _, trialNode := range lbNodes {
if trialNode.Address == address && trialNode.Port == port {
found = &trialNode
return false, nil
}
}
return true, nil
})
if found == nil {
return nil, errors.New("Unable to get node by IP and Port")
}
return found, nil
}

View File

@ -36,6 +36,10 @@ The following arguments are supported:
(must be "true" or "false" if provided). Changing this updates the (must be "true" or "false" if provided). Changing this updates the
`admin_state_up` of an existing router. `admin_state_up` of an existing router.
* `distributed` - (Optional) Indicates whether or not to create a
distributed router. The default policy setting in Neutron restricts
usage of this property to administrative users only.
* `external_gateway` - (Optional) The network UUID of an external gateway for * `external_gateway` - (Optional) The network UUID of an external gateway for
the router. A router with an external gateway is required if any compute the router. A router with an external gateway is required if any compute
instances or load balancers will be using floating IPs. Changing this instances or load balancers will be using floating IPs. Changing this