Adding security group rules
diff --git a/openstack/networking/v2/extensions/security/rules/requests.go b/openstack/networking/v2/extensions/security/rules/requests.go
index 1e8c7fa..ea0f37d 100644
--- a/openstack/networking/v2/extensions/security/rules/requests.go
+++ b/openstack/networking/v2/extensions/security/rules/requests.go
@@ -1 +1,183 @@
package rules
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the security group attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Direction string `q:"direction"`
+ EtherType string `q:"ethertype"`
+ ID string `q:"id"`
+ PortRangeMax int `q:"port_range_max"`
+ PortRangeMin int `q:"port_range_min"`
+ Protocol string `q:"protocol"`
+ RemoteGroupID string `q:"remote_group_id"`
+ RemoteIPPrefix string `q:"remote_ip_prefix"`
+ SecGroupID string `q:"security_group_id"`
+ TenantID string `q:"tenant_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// security group rules. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return SecGroupRulePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// Errors
+var (
+ errValidDirectionRequired = fmt.Errorf("A valid Direction is required")
+ errValidEtherTypeRequired = fmt.Errorf("A valid EtherType is required")
+ errSecGroupIDRequired = fmt.Errorf("A valid SecGroupID is required")
+ errValidProtocolRequired = fmt.Errorf("A valid Protocol is required")
+)
+
+// Constants useful for CreateOpts
+const (
+ DirIngress = "ingress"
+ DirEgress = "egress"
+ Ether4 = "IPv4"
+ Ether6 = "IPv6"
+ ProtocolTCP = "tcp"
+ ProtocolUDP = "udp"
+ ProtocolICMP = "icmp"
+)
+
+// CreateOpts contains all the values needed to create a new security group rule.
+type CreateOpts struct {
+ // Required. Must be either "ingress" or "egress": the direction in which the
+ // security group rule is applied.
+ Direction string
+
+ // Required. Must be "IPv4" or "IPv6", and addresses represented in CIDR must
+ // match the ingress or egress rules.
+ EtherType string
+
+ // Required. The security group ID to associate with this security group rule.
+ SecGroupID string
+
+ // Optional. The maximum port number in the range that is matched by the
+ // security group rule. The PortRangeMin attribute constrains the PortRangeMax
+ // attribute. If the protocol is ICMP, this value must be an ICMP type.
+ PortRangeMax int
+
+ // Optional. The minimum port number in the range that is matched by the
+ // security group rule. If the protocol is TCP or UDP, this value must be
+ // less than or equal to the value of the PortRangeMax attribute. If the
+ // protocol is ICMP, this value must be an ICMP type.
+ PortRangeMin int
+
+ // Optional. The protocol that is matched by the security group rule. Valid
+ // values are "tcp", "udp", "icmp" or an empty string.
+ Protocol string
+
+ // Optional. The remote group ID to be associated with this security group
+ // rule. You can specify either RemoteGroupID or RemoteIPPrefix.
+ RemoteGroupID string
+
+ // Optional. The remote IP prefix to be associated with this security group
+ // rule. You can specify either RemoteGroupID or RemoteIPPrefix. This
+ // attribute matches the specified IP prefix as the source IP address of the
+ // IP packet.
+ RemoteIPPrefix string
+}
+
+// Create is an operation which provisions a new security group with default
+// security group rules for the IPv4 and IPv6 ether types.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate required opts
+ if opts.Direction != DirIngress && opts.Direction != DirEgress {
+ res.Err = errValidDirectionRequired
+ return res
+ }
+ if opts.EtherType != Ether4 && opts.EtherType != Ether6 {
+ res.Err = errValidEtherTypeRequired
+ return res
+ }
+ if opts.SecGroupID == "" {
+ res.Err = errSecGroupIDRequired
+ return res
+ }
+ if opts.Protocol != "" && opts.Protocol != ProtocolTCP && opts.Protocol != ProtocolUDP && opts.Protocol != ProtocolICMP {
+ res.Err = errValidProtocolRequired
+ return res
+ }
+
+ type secrule struct {
+ Direction string `json:"direction"`
+ EtherType string `json:"ethertype"`
+ SecGroupID string `json:"security_group_id"`
+ PortRangeMax int `json:"port_range_max,omitempty"`
+ PortRangeMin int `json:"port_range_min,omitempty"`
+ Protocol string `json:"protocol,omitempty"`
+ RemoteGroupID string `json:"remote_group_id,omitempty"`
+ RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"`
+ }
+
+ type request struct {
+ SecRule secrule `json:"security_group_rule"`
+ }
+
+ reqBody := request{SecRule: secrule{
+ Direction: opts.Direction,
+ EtherType: opts.EtherType,
+ SecGroupID: opts.SecGroupID,
+ PortRangeMax: opts.PortRangeMax,
+ PortRangeMin: opts.PortRangeMin,
+ Protocol: opts.Protocol,
+ RemoteGroupID: opts.RemoteGroupID,
+ RemoteIPPrefix: opts.RemoteIPPrefix,
+ }}
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/openstack/networking/v2/extensions/security/rules/requests_test.go b/openstack/networking/v2/extensions/security/rules/requests_test.go
index 1e8c7fa..cb99054 100644
--- a/openstack/networking/v2/extensions/security/rules/requests_test.go
+++ b/openstack/networking/v2/extensions/security/rules/requests_test.go
@@ -1 +1,233 @@
package rules
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenID = "123"
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenID},
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules", rootURL(serviceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules/foo", resourceURL(serviceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "93aa42e5-80db-4581-9391-3a608bd0e448",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(serviceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractRules(page)
+ if err != nil {
+ t.Errorf("Failed to extract secrules: %v", err)
+ return false, err
+ }
+
+ expected := []SecGroupRule{
+ SecGroupRule{
+ Direction: "egress",
+ EtherType: "IPv6",
+ ID: "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ PortRangeMax: 0,
+ PortRangeMin: 0,
+ Protocol: "",
+ RemoteGroupID: "",
+ RemoteIPPrefix: "",
+ SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ TenantID: "e4f50856753b4dc6afee5fa6b9b6c550",
+ },
+ SecGroupRule{
+ Direction: "egress",
+ EtherType: "IPv4",
+ ID: "93aa42e5-80db-4581-9391-3a608bd0e448",
+ PortRangeMax: 0,
+ PortRangeMin: 0,
+ Protocol: "",
+ RemoteGroupID: "",
+ RemoteIPPrefix: "",
+ SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ TenantID: "e4f50856753b4dc6afee5fa6b9b6c550",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "security_group_rule": {
+ "direction": "ingress",
+ "port_range_min": 80,
+ "ethertype": "IPv4",
+ "port_range_max": 80,
+ "protocol": "tcp",
+ "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_rule": {
+ "direction": "ingress",
+ "ethertype": "IPv4",
+ "id": "2bc0accf-312e-429a-956e-e4407625eb62",
+ "port_range_max": 80,
+ "port_range_min": 80,
+ "protocol": "tcp",
+ "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "remote_ip_prefix": null,
+ "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{
+ Direction: "ingress",
+ PortRangeMin: 80,
+ EtherType: "IPv4",
+ PortRangeMax: 80,
+ Protocol: "tcp",
+ RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a",
+ }
+ _, err := Create(serviceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules/3c0e45ff-adaf-4124-b083-bf390e5482ff", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", tokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_rule": {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ sr, err := Get(serviceClient(), "3c0e45ff-adaf-4124-b083-bf390e5482ff").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "egress", sr.Direction)
+ th.AssertEquals(t, "IPv6", sr.EtherType)
+ th.AssertEquals(t, "3c0e45ff-adaf-4124-b083-bf390e5482ff", sr.ID)
+ th.AssertEquals(t, 0, sr.PortRangeMax)
+ th.AssertEquals(t, 0, sr.PortRangeMin)
+ th.AssertEquals(t, "", sr.Protocol)
+ th.AssertEquals(t, "", sr.RemoteGroupID)
+ th.AssertEquals(t, "", sr.RemoteIPPrefix)
+ th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sr.SecGroupID)
+ th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sr.TenantID)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", tokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := Delete(serviceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go
index 8e948e7..13d5c7d 100644
--- a/openstack/networking/v2/extensions/security/rules/results.go
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -1,3 +1,153 @@
package rules
-type SecGroupRule struct{}
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SecGroupRule represents a rule to dictate the behaviour of incoming or
+// outgoing traffic for a particular security group.
+type SecGroupRule struct {
+ // The UUID for this security group rule.
+ ID string
+
+ // The direction in which the security group rule is applied. The only values
+ // allowed are "ingress" or "egress". For a compute instance, an ingress
+ // security group rule is applied to incoming (ingress) traffic for that
+ // instance. An egress rule is applied to traffic leaving the instance.
+ Direction string
+
+ // Must be IPv4 or IPv6, and addresses represented in CIDR must match the
+ // ingress or egress rules.
+ EtherType string `json:"ethertype" mapstructure:"ethertype"`
+
+ // The security group ID to associate with this security group rule.
+ SecGroupID string `json:"security_group_id" mapstructure:"security_group_id"`
+
+ // The minimum port number in the range that is matched by the security group
+ // rule. If the protocol is TCP or UDP, this value must be less than or equal
+ // to the value of the PortRangeMax attribute. If the protocol is ICMP, this
+ // value must be an ICMP type.
+ PortRangeMin int `json:"port_range_min" mapstructure:"port_range_min"`
+
+ // The maximum port number in the range that is matched by the security group
+ // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If
+ // the protocol is ICMP, this value must be an ICMP type.
+ PortRangeMax int `json:"port_range_max" mapstructure:"port_range_max"`
+
+ // The protocol that is matched by the security group rule. Valid values are
+ // "tcp", "udp", "icmp" or an empty string.
+ Protocol string
+
+ // The remote group ID to be associated with this security group rule. You
+ // can specify either RemoteGroupID or RemoteIPPrefix.
+ RemoteGroupID string `json:"remote_group_id" mapstructure:"remote_group_id"`
+
+ // The remote IP prefix to be associated with this security group rule. You
+ // can specify either RemoteGroupID or RemoteIPPrefix . This attribute
+ // matches the specified IP prefix as the source IP address of the IP packet.
+ RemoteIPPrefix string `json:"remote_ip_prefix" mapstructure:"remote_ip_prefix"`
+
+ // The owner of this security group rule.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// SecGroupRulePage is the page returned by a pager when traversing over a
+// collection of security group rules.
+type SecGroupRulePage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of security group rules has
+// reached the end of a page and the pager seeks to traverse over a new one. In
+// order to do this, it needs to construct the next page's URL.
+func (p SecGroupRulePage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"security_group_rules_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// IsEmpty checks whether a SecGroupRulePage struct is empty.
+func (p SecGroupRulePage) IsEmpty() (bool, error) {
+ is, err := ExtractRules(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractRules accepts a Page struct, specifically a SecGroupRulePage struct,
+// and extracts the elements into a slice of SecGroupRule structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractRules(page pagination.Page) ([]SecGroupRule, error) {
+ var resp struct {
+ SecGroupRules []SecGroupRule `mapstructure:"security_group_rules" json:"security_group_rules"`
+ }
+
+ err := mapstructure.Decode(page.(SecGroupRulePage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.SecGroupRules, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a security rule.
+func (r commonResult) Extract() (*SecGroupRule, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ SecGroupRule *SecGroupRule `mapstructure:"security_group_rule" json:"security_group_rule"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron SecGroupRule: %v", err)
+ }
+
+ return res.SecGroupRule, nil
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/openstack/networking/v2/extensions/security/rules/urls.go b/openstack/networking/v2/extensions/security/rules/urls.go
index 1e8c7fa..2ffbf37 100644
--- a/openstack/networking/v2/extensions/security/rules/urls.go
+++ b/openstack/networking/v2/extensions/security/rules/urls.go
@@ -1 +1,16 @@
package rules
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ rootPath = "security-group-rules"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(version, rootPath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, rootPath, id)
+}