Merge pull request #3 from jrperritt/rackspace-gophercloud-commits
Rackspace gophercloud commits
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index becaf44..2cf355b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,7 +11,8 @@
way than just downloading it. Here are the basic installation instructions:
1. Configure your `$GOPATH` and run `go get` as described in the main
-[README](/README.md#how-to-install).
+[README](/README.md#how-to-install) but add `-tags "fixtures acceptance"` to
+get dependencies for unit and acceptance tests.
2. Move into the directory that houses your local repository:
@@ -158,25 +159,25 @@
To run all tests:
```bash
-go test ./...
+go test -tags fixtures ./...
```
To run all tests with verbose output:
```bash
-go test -v ./...
+go test -v -tags fixtures ./...
```
To run tests that match certain [build tags]():
```bash
-go test -tags "foo bar" ./...
+go test -tags "fixtures foo bar" ./...
```
To run tests for a particular sub-package:
```bash
-cd ./path/to/package && go test .
+cd ./path/to/package && go test -tags fixtures .
```
## Style guide
diff --git a/acceptance/openstack/compute/v2/quotaset_test.go b/acceptance/openstack/compute/v2/quotaset_test.go
new file mode 100644
index 0000000..3851edf
--- /dev/null
+++ b/acceptance/openstack/compute/v2/quotaset_test.go
@@ -0,0 +1,60 @@
+// +build acceptance compute
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/quotasets"
+ "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestGetQuotaset(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ idclient := openstack.NewIdentityV2(client.ProviderClient)
+ quotaset, err := quotasets.Get(client, findTenant(t, idclient)).Extract()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Logf("QuotaSet details:\n")
+ t.Logf(" instances=[%d]\n", quotaset.Instances)
+ t.Logf(" cores=[%d]\n", quotaset.Cores)
+ t.Logf(" ram=[%d]\n", quotaset.Ram)
+ t.Logf(" key_pairs=[%d]\n", quotaset.KeyPairs)
+ t.Logf(" metadata_items=[%d]\n", quotaset.MetadataItems)
+ t.Logf(" security_groups=[%d]\n", quotaset.SecurityGroups)
+ t.Logf(" security_group_rules=[%d]\n", quotaset.SecurityGroupRules)
+ t.Logf(" fixed_ips=[%d]\n", quotaset.FixedIps)
+ t.Logf(" floating_ips=[%d]\n", quotaset.FloatingIps)
+ t.Logf(" injected_file_content_bytes=[%d]\n", quotaset.InjectedFileContentBytes)
+ t.Logf(" injected_file_path_bytes=[%d]\n", quotaset.InjectedFilePathBytes)
+ t.Logf(" injected_files=[%d]\n", quotaset.InjectedFiles)
+
+}
+
+func findTenant(t *testing.T, client *gophercloud.ServiceClient) string {
+ var tenantID string
+ err := tenants.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ tenantList, err := tenants.ExtractTenants(page)
+ th.AssertNoErr(t, err)
+
+ for _, t := range tenantList {
+ tenantID = t.ID
+ break
+ }
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ return tenantID
+}
diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go
index 3100b1f..da06c35 100644
--- a/acceptance/openstack/compute/v2/secgroup_test.go
+++ b/acceptance/openstack/compute/v2/secgroup_test.go
@@ -107,6 +107,24 @@
th.AssertNoErr(t, err)
t.Logf("Deleted rule %s from group %s", rule.ID, id)
+
+ icmpOpts := secgroups.CreateRuleOpts{
+ ParentGroupID: id,
+ FromPort: 0,
+ ToPort: 0,
+ IPProtocol: "ICMP",
+ CIDR: "0.0.0.0/0",
+ }
+
+ icmpRule, err := secgroups.CreateRule(client, icmpOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Adding ICMP rule %s to group %s", icmpRule.ID, id)
+
+ err = secgroups.DeleteRule(client, icmpRule.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Deleted ICMP rule %s from group %s", icmpRule.ID, id)
}
func findServer(t *testing.T, client *gophercloud.ServiceClient) (string, bool) {
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/common.go b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
index 045a8ae..ed948bd 100644
--- a/acceptance/openstack/networking/v2/extensions/lbaas/common.go
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
@@ -3,6 +3,7 @@
import (
"testing"
+ "github.com/gophercloud/gophercloud"
base "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
@@ -22,7 +23,7 @@
s, err := subnets.Create(base.Client, subnets.CreateOpts{
NetworkID: n.ID,
CIDR: "192.168.199.0/24",
- IPVersion: subnets.IPv4,
+ IPVersion: gophercloud.IPv4,
Name: "tmp_subnet",
}).Extract()
th.AssertNoErr(t, err)
diff --git a/acceptance/openstack/networking/v2/subnet_test.go b/acceptance/openstack/networking/v2/subnet_test.go
index f6ce17e..c9efd5c 100644
--- a/acceptance/openstack/networking/v2/subnet_test.go
+++ b/acceptance/openstack/networking/v2/subnet_test.go
@@ -11,7 +11,7 @@
th "github.com/gophercloud/gophercloud/testhelper"
)
-func TestList(t *testing.T) {
+func TestSubnetList(t *testing.T) {
Setup(t)
defer Teardown()
@@ -32,7 +32,7 @@
th.CheckNoErr(t, err)
}
-func TestCRUD(t *testing.T) {
+func TestSubnetCRUD(t *testing.T) {
Setup(t)
defer Teardown()
@@ -61,6 +61,7 @@
th.AssertEquals(t, s.IPVersion, 4)
th.AssertEquals(t, s.Name, "my_subnet")
th.AssertEquals(t, s.EnableDHCP, false)
+ th.AssertEquals(t, s.GatewayIP, "192.168.199.1")
subnetID := s.ID
// Get subnet
@@ -79,6 +80,60 @@
t.Log("Delete subnet")
res := subnets.Delete(Client, subnetID)
th.AssertNoErr(t, res.Err)
+
+ // Create subnet with no gateway
+ t.Log("Create subnet with no gateway")
+ opts = subnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: "192.168.199.0/24",
+ IPVersion: subnets.IPv4,
+ Name: "my_subnet",
+ EnableDHCP: &enable,
+ NoGateway: true,
+ }
+ s, err = subnets.Create(Client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.NetworkID, networkID)
+ th.AssertEquals(t, s.CIDR, "192.168.199.0/24")
+ th.AssertEquals(t, s.IPVersion, 4)
+ th.AssertEquals(t, s.Name, "my_subnet")
+ th.AssertEquals(t, s.EnableDHCP, false)
+ th.AssertEquals(t, s.GatewayIP, "")
+ subnetID = s.ID
+
+ // Get subnet
+ t.Log("Getting subnet with no gateway")
+ s, err = subnets.Get(Client, subnetID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, s.ID, subnetID)
+
+ // Update subnet
+ t.Log("Update subnet with no gateway")
+ s, err = subnets.Update(Client, subnetID, subnets.UpdateOpts{Name: "new_subnet_name"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, s.Name, "new_subnet_name")
+
+ // Delete subnet
+ t.Log("Delete subnet with no gateway")
+ res = subnets.Delete(Client, subnetID)
+ th.AssertNoErr(t, res.Err)
+
+ // Create subnet with invalid gateway configuration
+ t.Log("Create subnet with invalid gateway configuration")
+ opts = subnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: "192.168.199.0/24",
+ IPVersion: subnets.IPv4,
+ Name: "my_subnet",
+ EnableDHCP: &enable,
+ NoGateway: true,
+ GatewayIP: "192.168.199.1",
+ }
+ _, err = subnets.Create(Client, opts).Extract()
+ if err == nil {
+ t.Fatalf("Expected an error, got none")
+ }
}
func TestBatchCreate(t *testing.T) {
diff --git a/openstack/blockstorage/v1/apiversions/urls.go b/openstack/blockstorage/v1/apiversions/urls.go
index 936f1c9..c9cf895 100644
--- a/openstack/blockstorage/v1/apiversions/urls.go
+++ b/openstack/blockstorage/v1/apiversions/urls.go
@@ -2,6 +2,7 @@
import (
"strings"
+ "net/url"
"github.com/gophercloud/gophercloud"
)
@@ -11,5 +12,7 @@
}
func listURL(c *gophercloud.ServiceClient) string {
- return c.ServiceURL("")
+ u, _ := url.Parse(c.ServiceURL(""))
+ u.Path = "/"
+ return u.String()
}
diff --git a/openstack/blockstorage/v1/snapshots/fixtures.go b/openstack/blockstorage/v1/snapshots/fixtures.go
index b1bfef8..1dcdfca 100644
--- a/openstack/blockstorage/v1/snapshots/fixtures.go
+++ b/openstack/blockstorage/v1/snapshots/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package snapshots
import (
diff --git a/openstack/blockstorage/v1/volumes/testing/fixtures.go b/openstack/blockstorage/v1/volumes/testing/fixtures.go
index 421cbf4..0d34d5e 100644
--- a/openstack/blockstorage/v1/volumes/testing/fixtures.go
+++ b/openstack/blockstorage/v1/volumes/testing/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package testing
import (
diff --git a/openstack/blockstorage/v1/volumetypes/fixtures.go b/openstack/blockstorage/v1/volumetypes/fixtures.go
index 1969120..cb6fadf 100644
--- a/openstack/blockstorage/v1/volumetypes/fixtures.go
+++ b/openstack/blockstorage/v1/volumetypes/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package volumetypes
import (
diff --git a/openstack/cdn/v1/base/fixtures.go b/openstack/cdn/v1/base/fixtures.go
index f95d893..226edae 100644
--- a/openstack/cdn/v1/base/fixtures.go
+++ b/openstack/cdn/v1/base/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package base
import (
diff --git a/openstack/cdn/v1/flavors/fixtures.go b/openstack/cdn/v1/flavors/fixtures.go
index 285075e..5d07491 100644
--- a/openstack/cdn/v1/flavors/fixtures.go
+++ b/openstack/cdn/v1/flavors/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package flavors
import (
diff --git a/openstack/cdn/v1/serviceassets/fixtures.go b/openstack/cdn/v1/serviceassets/fixtures.go
index 9c62514..a66c503 100644
--- a/openstack/cdn/v1/serviceassets/fixtures.go
+++ b/openstack/cdn/v1/serviceassets/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package serviceassets
import (
diff --git a/openstack/cdn/v1/services/fixtures.go b/openstack/cdn/v1/services/fixtures.go
index c882f8c..12d260e 100644
--- a/openstack/cdn/v1/services/fixtures.go
+++ b/openstack/cdn/v1/services/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package services
import (
diff --git a/openstack/client.go b/openstack/client.go
index 2fa4750..3e11508 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -151,13 +151,17 @@
v3Client.Endpoint = endpoint
}
+ // copy the auth options to a local variable that we can change. `options`
+ // needs to stay as-is for reauth purposes
+ v3Options := options
+
var scope *tokens3.Scope
if options.TenantID != "" {
scope = &tokens3.Scope{
ProjectID: options.TenantID,
}
- options.TenantID = ""
- options.TenantName = ""
+ v3Options.TenantID = ""
+ v3Options.TenantName = ""
} else {
if options.TenantName != "" {
scope = &tokens3.Scope{
@@ -165,7 +169,7 @@
DomainID: options.DomainID,
DomainName: options.DomainName,
}
- options.TenantName = ""
+ v3Options.TenantName = ""
}
}
diff --git a/openstack/compute/v2/extensions/defsecrules/fixtures.go b/openstack/compute/v2/extensions/defsecrules/fixtures.go
index d35af0b..4fee896 100644
--- a/openstack/compute/v2/extensions/defsecrules/fixtures.go
+++ b/openstack/compute/v2/extensions/defsecrules/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package defsecrules
import (
@@ -72,6 +74,41 @@
})
}
+func mockCreateRuleResponseICMPZero(t *testing.T) {
+ th.Mux.HandleFunc(rootPath, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "security_group_default_rule": {
+ "ip_protocol": "ICMP",
+ "from_port": 0,
+ "to_port": 0,
+ "cidr": "10.10.12.0/24"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_default_rule": {
+ "from_port": 0,
+ "id": "{ruleID}",
+ "ip_protocol": "ICMP",
+ "ip_range": {
+ "cidr": "10.10.12.0/24"
+ },
+ "to_port": 0
+ }
+}
+`)
+ })
+}
+
func mockGetRuleResponse(t *testing.T, ruleID string) {
url := rootPath + "/" + ruleID
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
diff --git a/openstack/compute/v2/extensions/defsecrules/requests.go b/openstack/compute/v2/extensions/defsecrules/requests.go
index 6f24bb3..184fdc9 100644
--- a/openstack/compute/v2/extensions/defsecrules/requests.go
+++ b/openstack/compute/v2/extensions/defsecrules/requests.go
@@ -1,6 +1,8 @@
package defsecrules
import (
+ "strings"
+
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
@@ -14,10 +16,10 @@
// CreateOpts represents the configuration for adding a new default rule.
type CreateOpts struct {
- // The lower bound of the port range that will be opened.
- FromPort int `json:"from_port" required:"true"`
+ // The lower bound of the port range that will be opened.s
+ FromPort int `json:"from_port"`
// The upper bound of the port range that will be opened.
- ToPort int `json:"to_port" required:"true"`
+ ToPort int `json:"to_port"`
// The protocol type that will be allowed, e.g. TCP.
IPProtocol string `json:"ip_protocol" required:"true"`
// ONLY required if FromGroupID is blank. This represents the IP range that
@@ -33,6 +35,12 @@
// ToRuleCreateMap builds the create rule options into a serializable format.
func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+ if opts.FromPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
+ return nil, gophercloud.ErrMissingInput{Argument: "FromPort"}
+ }
+ if opts.ToPort == 0 && strings.ToUpper(opts.IPProtocol) != "ICMP" {
+ return nil, gophercloud.ErrMissingInput{Argument: "ToPort"}
+ }
return gophercloud.BuildRequestBody(opts, "security_group_default_rule")
}
diff --git a/openstack/compute/v2/extensions/defsecrules/requests_test.go b/openstack/compute/v2/extensions/defsecrules/requests_test.go
index 0e2a010..df568fe 100644
--- a/openstack/compute/v2/extensions/defsecrules/requests_test.go
+++ b/openstack/compute/v2/extensions/defsecrules/requests_test.go
@@ -69,6 +69,32 @@
th.AssertDeepEquals(t, expected, group)
}
+func TestCreateICMPZero(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateRuleResponseICMPZero(t)
+
+ opts := CreateOpts{
+ IPProtocol: "ICMP",
+ FromPort: 0,
+ ToPort: 0,
+ CIDR: "10.10.12.0/24",
+ }
+
+ group, err := Create(client.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &DefaultRule{
+ ID: ruleID,
+ FromPort: 0,
+ ToPort: 0,
+ IPProtocol: "ICMP",
+ IPRange: secgroups.IPRange{CIDR: "10.10.12.0/24"},
+ }
+ th.AssertDeepEquals(t, expected, group)
+}
+
func TestGet(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/compute/v2/extensions/quotasets/doc.go b/openstack/compute/v2/extensions/quotasets/doc.go
new file mode 100644
index 0000000..721024e
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/doc.go
@@ -0,0 +1,3 @@
+// Package quotasets provides information and interaction with QuotaSet
+// extension for the OpenStack Compute service.
+package quotasets
diff --git a/openstack/compute/v2/extensions/quotasets/fixtures.go b/openstack/compute/v2/extensions/quotasets/fixtures.go
new file mode 100644
index 0000000..c1bb4ea
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/fixtures.go
@@ -0,0 +1,59 @@
+// +build fixtures
+
+package quotasets
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+ "quota_set" : {
+ "instances" : 25,
+ "security_groups" : 10,
+ "security_group_rules" : 20,
+ "cores" : 200,
+ "injected_file_content_bytes" : 10240,
+ "injected_files" : 5,
+ "metadata_items" : 128,
+ "ram" : 200000,
+ "keypairs" : 10,
+ "injected_file_path_bytes" : 255
+ }
+}
+`
+
+const FirstTenantID = "555544443333222211110000ffffeeee"
+
+// FirstQuotaSet is the first result in ListOutput.
+var FirstQuotaSet = QuotaSet{
+ FixedIps: 0,
+ FloatingIps: 0,
+ InjectedFileContentBytes: 10240,
+ InjectedFilePathBytes: 255,
+ InjectedFiles: 5,
+ KeyPairs: 10,
+ MetadataItems: 128,
+ Ram: 200000,
+ SecurityGroupRules: 20,
+ SecurityGroups: 10,
+ Cores: 200,
+ Instances: 25,
+}
+
+// HandleGetSuccessfully configures the test server to respond to a Get request for sample tenant
+func HandleGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/os-quota-sets/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, GetOutput)
+ })
+}
diff --git a/openstack/compute/v2/extensions/quotasets/requests.go b/openstack/compute/v2/extensions/quotasets/requests.go
new file mode 100644
index 0000000..52f0839
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/requests.go
@@ -0,0 +1,12 @@
+package quotasets
+
+import (
+ "github.com/rackspace/gophercloud"
+)
+
+// Get returns public data about a previously created QuotaSet.
+func Get(client *gophercloud.ServiceClient, tenantID string) GetResult {
+ var res GetResult
+ _, res.Err = client.Get(getURL(client, tenantID), &res.Body, nil)
+ return res
+}
diff --git a/openstack/compute/v2/extensions/quotasets/requests_test.go b/openstack/compute/v2/extensions/quotasets/requests_test.go
new file mode 100644
index 0000000..5d766fa
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/requests_test.go
@@ -0,0 +1,16 @@
+package quotasets
+
+import (
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+ "testing"
+)
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t)
+ actual, err := Get(client.ServiceClient(), FirstTenantID).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &FirstQuotaSet, actual)
+}
diff --git a/openstack/compute/v2/extensions/quotasets/results.go b/openstack/compute/v2/extensions/quotasets/results.go
new file mode 100644
index 0000000..cbf4d6b
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/results.go
@@ -0,0 +1,86 @@
+package quotasets
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// QuotaSet is a set of operational limits that allow for control of compute usage.
+type QuotaSet struct {
+ //ID is tenant associated with this quota_set
+ ID string `mapstructure:"id"`
+ //FixedIps is number of fixed ips alloted this quota_set
+ FixedIps int `mapstructure:"fixed_ips"`
+ // FloatingIps is number of floating ips alloted this quota_set
+ FloatingIps int `mapstructure:"floating_ips"`
+ // InjectedFileContentBytes is content bytes allowed for each injected file
+ InjectedFileContentBytes int `mapstructure:"injected_file_content_bytes"`
+ // InjectedFilePathBytes is allowed bytes for each injected file path
+ InjectedFilePathBytes int `mapstructure:"injected_file_path_bytes"`
+ // InjectedFiles is injected files allowed for each project
+ InjectedFiles int `mapstructure:"injected_files"`
+ // KeyPairs is number of ssh keypairs
+ KeyPairs int `mapstructure:"keypairs"`
+ // MetadataItems is number of metadata items allowed for each instance
+ MetadataItems int `mapstructure:"metadata_items"`
+ // Ram is megabytes allowed for each instance
+ Ram int `mapstructure:"ram"`
+ // SecurityGroupRules is rules allowed for each security group
+ SecurityGroupRules int `mapstructure:"security_group_rules"`
+ // SecurityGroups security groups allowed for each project
+ SecurityGroups int `mapstructure:"security_groups"`
+ // Cores is number of instance cores allowed for each project
+ Cores int `mapstructure:"cores"`
+ // Instances is number of instances allowed for each project
+ Instances int `mapstructure:"instances"`
+}
+
+// QuotaSetPage stores a single, only page of QuotaSet results from a List call.
+type QuotaSetPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a QuotaSetsetPage is empty.
+func (page QuotaSetPage) IsEmpty() (bool, error) {
+ ks, err := ExtractQuotaSets(page)
+ return len(ks) == 0, err
+}
+
+// ExtractQuotaSets interprets a page of results as a slice of QuotaSets.
+func ExtractQuotaSets(page pagination.Page) ([]QuotaSet, error) {
+ var resp struct {
+ QuotaSets []QuotaSet `mapstructure:"quotas"`
+ }
+
+ err := mapstructure.Decode(page.(QuotaSetPage).Body, &resp)
+ results := make([]QuotaSet, len(resp.QuotaSets))
+ for i, q := range resp.QuotaSets {
+ results[i] = q
+ }
+ return results, err
+}
+
+type quotaResult struct {
+ gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any QuotaSet resource response as a QuotaSet struct.
+func (r quotaResult) Extract() (*QuotaSet, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ QuotaSet *QuotaSet `json:"quota_set" mapstructure:"quota_set"`
+ }
+
+ err := mapstructure.Decode(r.Body, &res)
+ return res.QuotaSet, err
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a QuotaSet.
+type GetResult struct {
+ quotaResult
+}
diff --git a/openstack/compute/v2/extensions/quotasets/urls.go b/openstack/compute/v2/extensions/quotasets/urls.go
new file mode 100644
index 0000000..c04d941
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/urls.go
@@ -0,0 +1,13 @@
+package quotasets
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "os-quota-sets"
+
+func resourceURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(resourcePath)
+}
+
+func getURL(c *gophercloud.ServiceClient, tenantID string) string {
+ return c.ServiceURL(resourcePath, tenantID)
+}
diff --git a/openstack/compute/v2/extensions/quotasets/urls_test.go b/openstack/compute/v2/extensions/quotasets/urls_test.go
new file mode 100644
index 0000000..f19a6ad
--- /dev/null
+++ b/openstack/compute/v2/extensions/quotasets/urls_test.go
@@ -0,0 +1,16 @@
+package quotasets
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetURL(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ c := client.ServiceClient()
+
+ th.CheckEquals(t, c.Endpoint+"os-quota-sets/wat", getURL(c, "wat"))
+}
diff --git a/openstack/compute/v2/extensions/secgroups/fixtures.go b/openstack/compute/v2/extensions/secgroups/fixtures.go
index 0f97ac8..e4ca587 100644
--- a/openstack/compute/v2/extensions/secgroups/fixtures.go
+++ b/openstack/compute/v2/extensions/secgroups/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package secgroups
import (
@@ -216,6 +218,42 @@
})
}
+func mockAddRuleResponseICMPZero(t *testing.T) {
+ th.Mux.HandleFunc("/os-security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ th.TestJSONRequest(t, r, `
+{
+ "security_group_rule": {
+ "from_port": 0,
+ "ip_protocol": "ICMP",
+ "to_port": 0,
+ "parent_group_id": "{groupID}",
+ "cidr": "0.0.0.0/0"
+ }
+} `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_rule": {
+ "from_port": 0,
+ "group": {},
+ "ip_protocol": "ICMP",
+ "to_port": 0,
+ "parent_group_id": "{groupID}",
+ "ip_range": {
+ "cidr": "0.0.0.0/0"
+ },
+ "id": "{ruleID}"
+ }
+}`)
+ })
+}
+
func mockDeleteRuleResponse(t *testing.T, ruleID string) {
url := fmt.Sprintf("/os-security-group-rules/%s", ruleID)
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
diff --git a/openstack/compute/v2/extensions/secgroups/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go
index 81993bd..ec8019f 100644
--- a/openstack/compute/v2/extensions/secgroups/requests.go
+++ b/openstack/compute/v2/extensions/secgroups/requests.go
@@ -101,23 +101,23 @@
// CreateRuleOpts represents the configuration for adding a new rule to an
// existing security group.
type CreateRuleOpts struct {
- // Required - the ID of the group that this rule will be added to.
+ // the ID of the group that this rule will be added to.
ParentGroupID string `json:"parent_group_id" required:"true"`
- // Required - the lower bound of the port range that will be opened.
- FromPort int `json:"from_port" required:"true"`
- // Required - the upper bound of the port range that will be opened.
- ToPort int `json:"to_port" required:"true"`
- // Required - the protocol type that will be allowed, e.g. TCP.
+ // the lower bound of the port range that will be opened.
+ FromPort int `json:"from_port"`
+ // the upper bound of the port range that will be opened.
+ ToPort int `json:"to_port"`
+ // the protocol type that will be allowed, e.g. TCP.
IPProtocol string `json:"ip_protocol" required:"true"`
// ONLY required if FromGroupID is blank. This represents the IP range that
// will be the source of network traffic to your security group. Use
// 0.0.0.0/0 to allow all IP addresses.
- CIDR string `json:"cidr,omitempty"`
+ CIDR string `json:"cidr,omitempty" or:"FromGroupID"`
// ONLY required if CIDR is blank. This value represents the ID of a group
// that forwards traffic to the parent group. So, instead of accepting
// network traffic from an entire IP range, you can instead refine the
// inbound source by an existing security group.
- FromGroupID string `json:"group_id,omitempty"`
+ FromGroupID string `json:"group_id,omitempty" or:"CIDR"`
}
// CreateRuleOptsBuilder builds the create rule options into a serializable format.
diff --git a/openstack/compute/v2/extensions/secgroups/requests_test.go b/openstack/compute/v2/extensions/secgroups/requests_test.go
index 9496d4a..bdbedcd 100644
--- a/openstack/compute/v2/extensions/secgroups/requests_test.go
+++ b/openstack/compute/v2/extensions/secgroups/requests_test.go
@@ -217,6 +217,36 @@
th.AssertDeepEquals(t, expected, rule)
}
+func TestAddRuleICMPZero(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockAddRuleResponseICMPZero(t)
+
+ opts := CreateRuleOpts{
+ ParentGroupID: groupID,
+ FromPort: 0,
+ ToPort: 0,
+ IPProtocol: "ICMP",
+ CIDR: "0.0.0.0/0",
+ }
+
+ rule, err := CreateRule(client.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := &Rule{
+ FromPort: 0,
+ ToPort: 0,
+ Group: Group{},
+ IPProtocol: "ICMP",
+ ParentGroupID: groupID,
+ IPRange: IPRange{CIDR: "0.0.0.0/0"},
+ ID: ruleID,
+ }
+
+ th.AssertDeepEquals(t, expected, rule)
+}
+
func TestDeleteRule(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/compute/v2/extensions/startstop/fixtures.go b/openstack/compute/v2/extensions/startstop/fixtures.go
index 7169f7f..c5b4290 100644
--- a/openstack/compute/v2/extensions/startstop/fixtures.go
+++ b/openstack/compute/v2/extensions/startstop/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package startstop
import (
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index 224b996..8b2c94d 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -236,6 +236,12 @@
}
`
+const ServerPasswordBody = `
+{
+ "password": "xlozO3wLCBRWAa2yDjCCVx8vwNPypxnypmRYDa/zErlQ+EzPe1S/Gz6nfmC52mOlOSCRuUOmG7kqqgejPof6M7bOezS387zjq4LSvvwp28zUknzy4YzfFGhnHAdai3TxUJ26pfQCYrq8UTzmKF2Bq8ioSEtVVzM0A96pDh8W2i7BOz6MdoiVyiev/I1K2LsuipfxSJR7Wdke4zNXJjHHP2RfYsVbZ/k9ANu+Nz4iIH8/7Cacud/pphH7EjrY6a4RZNrjQskrhKYed0YERpotyjYk1eDtRe72GrSiXteqCM4biaQ5w3ruS+AcX//PXk3uJ5kC7d67fPXaVz4WaQRYMg=="
+}
+`
+
var (
// ServerHerp is a Server struct that should correspond to the first result in ServerListBody.
ServerHerp = Server{
@@ -430,8 +436,8 @@
})
}
-// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server password
-// change request.
+// HandleServerForceDeletionSuccessfully sets up the test server to respond to a server force deletion
+// request.
func HandleServerForceDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/servers/asdfasdfasdf/action", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
@@ -704,3 +710,14 @@
w.WriteHeader(http.StatusAccepted)
})
}
+
+// HandlePasswordGetSuccessfully sets up the test server to respond to a password Get request.
+func HandlePasswordGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/servers/1234asdf/os-server-password", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, ServerPasswordBody)
+ })
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 27ab764..6e23ada 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -731,3 +731,9 @@
return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"}
}
}
+
+// GetPassword makes a request against the nova API to get the encrypted administrative password.
+func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) {
+ _, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil)
+ return
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 931ab36..9a5d701 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -142,6 +142,15 @@
th.AssertNoErr(t, res.Err)
}
+func TestGetPassword(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePasswordGetSuccessfully(t)
+
+ res := GetPassword(client.ServiceClient(), "1234asdf")
+ th.AssertNoErr(t, res.Err)
+}
+
func TestRebootServer(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index ff2e795..023d0dd 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -1,12 +1,15 @@
package servers
import (
+ "crypto/rsa"
+ "encoding/base64"
"fmt"
"net/url"
"path"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
+ "github.com/mitchellh/mapstructure"
)
type serverResult struct {
@@ -62,6 +65,47 @@
gophercloud.Result
}
+// GetPasswordResult represent the result of a get os-server-password operation.
+type GetPasswordResult struct {
+ gophercloud.Result
+}
+
+// ExtractPassword gets the encrypted password.
+// If privateKey != nil the password is decrypted with the private key.
+// If privateKey == nil the encrypted password is returned and can be decrypted with:
+// echo '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key>
+func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) {
+
+ if r.Err != nil {
+ return "", r.Err
+ }
+
+ var response struct {
+ Password string `mapstructure:"password"`
+ }
+
+ err := mapstructure.Decode(r.Body, &response)
+ if err == nil && privateKey != nil && response.Password != "" {
+ return decryptPassword(response.Password, privateKey)
+ }
+ return response.Password, err
+}
+
+func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) {
+ b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword)))
+
+ n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword))
+ if err != nil {
+ return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err)
+ }
+ password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n])
+ if err != nil {
+ return "", fmt.Errorf("Failed to decrypt password: %s", err)
+ }
+
+ return string(password), nil
+}
+
// ExtractImageID gets the ID of the newly created server image from the header
func (res CreateImageResult) ExtractImageID() (string, error) {
if res.Err != nil {
diff --git a/openstack/compute/v2/servers/results_test.go b/openstack/compute/v2/servers/results_test.go
new file mode 100644
index 0000000..5b56055
--- /dev/null
+++ b/openstack/compute/v2/servers/results_test.go
@@ -0,0 +1,99 @@
+// +build fixtures
+
+package servers
+
+import (
+ "crypto/rsa"
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "golang.org/x/crypto/ssh"
+)
+
+// Fail - No password in JSON.
+func TestExtractPassword_no_pwd_data(t *testing.T) {
+
+ var dejson interface{}
+ err := json.Unmarshal([]byte(`{ "Crappy data": ".-.-." }`), &dejson)
+ if err != nil {
+ t.Fatalf("%s", err)
+ }
+ resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+ pwd, err := resp.ExtractPassword(nil)
+ th.AssertEquals(t, pwd, "")
+}
+
+// Ok - return encrypted password when no private key is given.
+func TestExtractPassword_encrypted_pwd(t *testing.T) {
+
+ var dejson interface{}
+ sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+ err := json.Unmarshal(sejson, &dejson)
+ fmt.Printf("%v\n", dejson)
+ if err != nil {
+ t.Fatalf("%s", err)
+ }
+ resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+ pwd, err := resp.ExtractPassword(nil)
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw==", pwd)
+}
+
+// Ok - return decrypted password when private key is given.
+// Decrytion can be verified by:
+// echo "<enc_pwd>" | base64 -D | openssl rsautl -decrypt -inkey <privateKey.pem>
+func TestExtractPassword_decrypted_pwd(t *testing.T) {
+
+ privateKey, err := ssh.ParseRawPrivateKey([]byte(`
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAo1ODZgwMVdTJYim9UYuYhowoPMhGEuV5IRZjcJ315r7RBSC+
+yEiBb1V+jhf+P8fzAyU35lkBzZGDr7E3jxSesbOuYT8cItQS4ErUnI1LGuqvMxwv
+X3GMyE/HmOcaiODF1XZN3Ur5pMJdVknnmczgUsW0hT98Udrh3MQn9WSuh/6LRy6+
+x1QsKHOCLFPnkhWa3LKyxmpQq/Gvhz+6NLe+gt8MFullA5mKQxBJ/K6laVHeaMlw
+JG3GCX0EZhRlvzoV8koIBKZtbKFolFr8ZtxBm3R5LvnyrtOvp22sa+xeItUT5kG1
+ZnbGNdK87oYW+VigEUfzT/+8R1i6E2QIXoeZiQIDAQABAoIBAQCVZ70IqbbTAW8j
+RAlyQh/J3Qal65LmkFJJKUDX8TfT1/Q/G6BKeMEmxm+Zrmsfj1pHI1HKftt+YEG1
+g4jOc09kQXkgbmnfll6aHPn3J+1vdwXD3GGdjrL5PrnYrngAhJWU2r8J0x8hT8ew
+OrUJZXhDX6XuSpAAFRmOKUZgXbSmo4X+LZX76ACnarselJt5FL724ECvpWJ7xxC4
+FMzvp4RqMmNFvv/Uq9lE/EmoSk4dviYyIZZ16DbDNyc9k/sGqCAMktCEwZ3EQm//
+S5bkNhgP6oUXjluWy53aPRgykEylgDWo5SSdSEyKnw/fciU0xdprA9JrBGIcTyHS
+/k2kgD4xAoGBANTkJ88Q0YrxX3fZNZVqcn00XKTxPGmxN5LRs7eV743q30AxK5Db
+QU8iwaAA1IKUWV5DLhgUTNsDCOPUPue4aOSBD3/sj+WEmvIhj7afDL5didkYHsqf
+fDnhFHq7y/3i57d428C7BwwR79pGWVyi7vH3pfu9A1iwl1aNOae+zvbVAoGBAMRm
+AmwQ9fJ3Qc44jysFK/yliLRGdShjkMMah5G3JlrelwfPtwPwEL2EHHhJB/C1acMs
+n6Q6RaoF6WNSZUY65ksQg7aPOYf2X0FTFwQJvwDJ4qlWjmq7w+tQ0AoGJG+dVUmQ
+zHZ/Y+HokSXzz9c4oevk4v/rMgAQ00WHrTdtIhnlAoGBALIJJ72D7CkNGHCq5qPQ
+xHQukPejgolFGhufYXM7YX3GmPMe67cVlTVv9Isxhoa5N0+cUPT0LR3PGOUm/4Bb
+eOT3hZXOqLwhvE6XgI8Rzd95bClwgXekDoh80dqeKMdmta961BQGlKskaPiacmsF
+G1yhZV70P9Mwwy8vpbLB4GUNAoGAbTwbjsWkNfa0qCF3J8NZoszjCvnBQfSW2J1R
+1+8ZKyNwt0yFi3Ajr3TibNiZzPzp1T9lj29FvfpJxA9Y+sXZvthxmcFxizix5GB1
+ha5yCNtA8VSOI7lJkAFDpL+j1lyYyjD6N9JE2KqEyKoh6J+8F7sXsqW7CqRRDfQX
+mKNfey0CgYEAxcEoNoADN2hRl7qY9rbQfVvQb3RkoQkdHhl9gpLFCcV32IP8R4xg
+09NbQK5OmgcIuZhLVNzTmUHJbabEGeXqIFIV0DsqECAt3WzbDyKQO23VJysFD46c
+KSde3I0ybDz7iS2EtceKB7m4C0slYd+oBkm4efuF00rCOKDwpFq45m0=
+-----END RSA PRIVATE KEY-----
+`))
+ if err != nil {
+ t.Fatalf("Error parsing private key: %s\n", err)
+ }
+
+ var dejson interface{}
+ sejson := []byte(`{"password":"PP8EnwPO9DhEc8+O/6CKAkPF379mKsUsfFY6yyw0734XXvKsSdV9KbiHQ2hrBvzeZxtGMrlFaikVunCRizyLLWLMuOi4hoH+qy9F9sQid61gQIGkxwDAt85d/7Eau2/KzorFnZhgxArl7IiqJ67X6xjKkR3zur+Yp3V/mtVIehpPYIaAvPbcp2t4mQXl1I9J8yrQfEZOctLL1L4heDEVXnxvNihVLK6pivlVggp6SZCtjj9cduZGrYGsxsOCso1dqJQr7GCojfwvuLOoG0OYwEGuWVTZppxWxi/q1QgeHFhGKA5QUXlz7pS71oqpjYsTeViuHnfvlqb5TVYZpQ1haw=="}`)
+
+ err = json.Unmarshal(sejson, &dejson)
+ fmt.Printf("%v\n", dejson)
+ if err != nil {
+ t.Fatalf("%s", err)
+ }
+ resp := GetPasswordResult{gophercloud.Result{Body: dejson}}
+
+ pwd, err := resp.ExtractPassword(privateKey.(*rsa.PrivateKey))
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, "ruZKK0tqxRfYm5t7lSJq", pwd)
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index a11504c..e892e8d 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -45,3 +45,7 @@
func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string {
return client.ServiceURL("servers", id, "ips", network)
}
+
+func passwordURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("servers", id, "os-server-password")
+}
diff --git a/openstack/db/v1/configurations/fixtures.go b/openstack/db/v1/configurations/fixtures.go
index ae65416..9064c6c 100644
--- a/openstack/db/v1/configurations/fixtures.go
+++ b/openstack/db/v1/configurations/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package configurations
import (
diff --git a/openstack/db/v1/databases/fixtures.go b/openstack/db/v1/databases/fixtures.go
index 4b35062..c99f990 100644
--- a/openstack/db/v1/databases/fixtures.go
+++ b/openstack/db/v1/databases/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package databases
import (
diff --git a/openstack/db/v1/datastores/fixtures.go b/openstack/db/v1/datastores/fixtures.go
index 837b1f4..8caa586 100644
--- a/openstack/db/v1/datastores/fixtures.go
+++ b/openstack/db/v1/datastores/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package datastores
import (
diff --git a/openstack/db/v1/flavors/fixtures.go b/openstack/db/v1/flavors/fixtures.go
index 6a013f9..257b214 100644
--- a/openstack/db/v1/flavors/fixtures.go
+++ b/openstack/db/v1/flavors/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package flavors
import (
diff --git a/openstack/db/v1/instances/fixtures.go b/openstack/db/v1/instances/fixtures.go
index d0a3856..6be384c 100644
--- a/openstack/db/v1/instances/fixtures.go
+++ b/openstack/db/v1/instances/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package instances
import (
diff --git a/openstack/db/v1/users/fixtures.go b/openstack/db/v1/users/fixtures.go
index 3b27005..3661154 100644
--- a/openstack/db/v1/users/fixtures.go
+++ b/openstack/db/v1/users/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package users
import (
diff --git a/openstack/identity/v2/extensions/admin/roles/fixtures.go b/openstack/identity/v2/extensions/admin/roles/fixtures.go
index 519dfae..6b11f5c 100644
--- a/openstack/identity/v2/extensions/admin/roles/fixtures.go
+++ b/openstack/identity/v2/extensions/admin/roles/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package roles
import (
diff --git a/openstack/identity/v2/users/fixtures.go b/openstack/identity/v2/users/fixtures.go
index ecd1768..7b0bc4c 100644
--- a/openstack/identity/v2/users/fixtures.go
+++ b/openstack/identity/v2/users/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package users
import (
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests.go b/openstack/networking/v2/extensions/layer3/routers/requests.go
index 48c0a52..71b2f62 100644
--- a/openstack/networking/v2/extensions/layer3/routers/requests.go
+++ b/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -40,6 +40,10 @@
})
}
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
type CreateOptsBuilder interface {
ToRouterCreateMap() (map[string]interface{}, error)
}
diff --git a/openstack/networking/v2/subnets/errors.go b/openstack/networking/v2/subnets/errors.go
index 0db0a6e..d2f7b46 100644
--- a/openstack/networking/v2/subnets/errors.go
+++ b/openstack/networking/v2/subnets/errors.go
@@ -7,7 +7,8 @@
}
var (
- errNetworkIDRequired = err("A network ID is required")
- errCIDRRequired = err("A valid CIDR is required")
- errInvalidIPType = err("An IP type must either be 4 or 6")
+ errNetworkIDRequired = err("A network ID is required")
+ errCIDRRequired = err("A valid CIDR is required")
+ errInvalidIPType = err("An IP type must either be 4 or 6")
+ errInvalidGatewayConfig = err("Both disabling the gateway and specifying a gateway is not allowed")
)
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
index 2706a5e..769474d 100644
--- a/openstack/networking/v2/subnets/requests.go
+++ b/openstack/networking/v2/subnets/requests.go
@@ -64,16 +64,6 @@
return
}
-// IPVersion is the IP address version for the subnet. Valid instances are
-// 4 and 6
-type IPVersion int
-
-// Valid IP types
-const (
- IPv4 IPVersion = 4
- IPv6 IPVersion = 6
-)
-
// CreateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Create operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
@@ -84,16 +74,16 @@
// CreateOpts represents the attributes used when creating a new subnet.
type CreateOpts struct {
- NetworkID string `json:"network_id" required:"true"`
- CIDR string `json:"cidr" required:"true"`
- Name string `json:"name,omitempty"`
- TenantID string `json:"tenant_id,omitempty"`
- AllocationPools []AllocationPool `json:"allocation_pools,omitempty"`
- GatewayIP string `json:"gateway_ip,omitempty"`
- IPVersion IPVersion `json:"ip_version,omitempty"`
- EnableDHCP *bool `json:"enable_dhcp,omitempty"`
- DNSNameservers []string `json:"dns_nameservers,omitempty"`
- HostRoutes []HostRoute `json:"host_routes,omitempty"`
+ NetworkID string `json:"network_id" required:"true"`
+ CIDR string `json:"cidr" required:"true"`
+ Name string `json:"name,omitempty"`
+ TenantID string `json:"tenant_id,omitempty"`
+ AllocationPools []AllocationPool `json:"allocation_pools,omitempty"`
+ GatewayIP *string `json:"gateway_ip"`
+ IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"`
+ EnableDHCP *bool `json:"enable_dhcp,omitempty"`
+ DNSNameservers []string `json:"dns_nameservers,omitempty"`
+ HostRoutes []HostRoute `json:"host_routes,omitempty"`
}
// ToSubnetCreateMap casts a CreateOpts struct to a map.
diff --git a/openstack/networking/v2/subnets/requests_test.go b/openstack/networking/v2/subnets/requests_test.go
index 5178c90..4241c63 100644
--- a/openstack/networking/v2/subnets/requests_test.go
+++ b/openstack/networking/v2/subnets/requests_test.go
@@ -59,6 +59,24 @@
"gateway_ip": "192.0.0.1",
"cidr": "192.0.0.0/8",
"id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ },
+ {
+ "name": "my_gatewayless_subnet",
+ "enable_dhcp": true,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "192.168.1.2",
+ "end": "192.168.1.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": null,
+ "cidr": "192.168.1.0/24",
+ "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c"
}
]
}
@@ -112,6 +130,24 @@
CIDR: "192.0.0.0/8",
ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0b",
},
+ Subnet{
+ Name: "my_gatewayless_subnet",
+ EnableDHCP: true,
+ NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ TenantID: "4fd44f30292945e481c7b8a0c8908869",
+ DNSNameservers: []string{},
+ AllocationPools: []AllocationPool{
+ AllocationPool{
+ Start: "192.168.1.2",
+ End: "192.168.1.254",
+ },
+ },
+ HostRoutes: []HostRoute{},
+ IPVersion: 4,
+ GatewayIP: "",
+ CIDR: "192.168.1.0/24",
+ ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0c",
+ },
}
th.CheckDeepEquals(t, expected, actual)
@@ -194,6 +230,7 @@
"subnet": {
"network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
"ip_version": 4,
+ "gateway_ip": null,
"cidr": "192.168.199.0/24",
"dns_nameservers": ["foo"],
"allocation_pools": [
@@ -270,6 +307,90 @@
th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126")
}
+func TestCreateNoGateway(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet": {
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ "ip_version": 4,
+ "cidr": "192.168.1.0/24",
+ "gateway_ip": null,
+ "allocation_pools": [
+ {
+ "start": "192.168.1.2",
+ "end": "192.168.1.254"
+ }
+ ]
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "subnet": {
+ "name": "",
+ "enable_dhcp": true,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "allocation_pools": [
+ {
+ "start": "192.168.1.2",
+ "end": "192.168.1.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": null,
+ "cidr": "192.168.1.0/24",
+ "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{
+ NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ IPVersion: 4,
+ CIDR: "192.168.1.0/24",
+ AllocationPools: []AllocationPool{
+ AllocationPool{
+ Start: "192.168.1.2",
+ End: "192.168.1.254",
+ },
+ },
+ DNSNameservers: []string{},
+ }
+ s, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "")
+ th.AssertEquals(t, s.EnableDHCP, true)
+ th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a23")
+ th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869")
+ th.AssertDeepEquals(t, s.AllocationPools, []AllocationPool{
+ AllocationPool{
+ Start: "192.168.1.2",
+ End: "192.168.1.254",
+ },
+ })
+ th.AssertDeepEquals(t, s.HostRoutes, []HostRoute{})
+ th.AssertEquals(t, s.IPVersion, 4)
+ th.AssertEquals(t, s.GatewayIP, "")
+ th.AssertEquals(t, s.CIDR, "192.168.1.0/24")
+ th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0c")
+}
+
func TestRequiredCreateOpts(t *testing.T) {
res := Create(fake.ServiceClient(), CreateOpts{})
if res.Err == nil {
diff --git a/openstack/orchestration/v1/buildinfo/fixtures.go b/openstack/orchestration/v1/buildinfo/fixtures.go
index 4e93126..87bc3ec 100644
--- a/openstack/orchestration/v1/buildinfo/fixtures.go
+++ b/openstack/orchestration/v1/buildinfo/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package buildinfo
import (
diff --git a/openstack/orchestration/v1/stackevents/fixtures.go b/openstack/orchestration/v1/stackevents/fixtures.go
index 48524e5..e4af1bb 100644
--- a/openstack/orchestration/v1/stackevents/fixtures.go
+++ b/openstack/orchestration/v1/stackevents/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package stackevents
import (
diff --git a/openstack/orchestration/v1/stackresources/fixtures.go b/openstack/orchestration/v1/stackresources/fixtures.go
index a622f7f..beec637 100644
--- a/openstack/orchestration/v1/stackresources/fixtures.go
+++ b/openstack/orchestration/v1/stackresources/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package stackresources
import (
diff --git a/openstack/orchestration/v1/stacks/fixtures.go b/openstack/orchestration/v1/stacks/fixtures.go
index f1d66f4..4126cf6 100644
--- a/openstack/orchestration/v1/stacks/fixtures.go
+++ b/openstack/orchestration/v1/stacks/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package stacks
import (
diff --git a/openstack/orchestration/v1/stacktemplates/fixtures.go b/openstack/orchestration/v1/stacktemplates/fixtures.go
index bfcaa90..4444a57 100644
--- a/openstack/orchestration/v1/stacktemplates/fixtures.go
+++ b/openstack/orchestration/v1/stacktemplates/fixtures.go
@@ -1,3 +1,5 @@
+// +build fixtures
+
package stacktemplates
import (