Adding security groups :lock:
diff --git a/openstack/networking/v2/extensions/security/doc.go b/openstack/networking/v2/extensions/security/doc.go
new file mode 100644
index 0000000..8ef455f
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/doc.go
@@ -0,0 +1,32 @@
+// Package security contains functionality to work with security group and
+// security group rules Neutron resources.
+//
+// Security groups and security group rules allows administrators and tenants
+// the ability to specify the type of traffic and direction (ingress/egress)
+// that is allowed to pass through a port. A security group is a container for
+// security group rules.
+//
+// When a port is created in Networking it is associated with a security group.
+// If a security group is not specified the port is associated with a 'default'
+// security group. By default, this group drops all ingress traffic and allows
+// all egress. Rules can be added to this group in order to change the behaviour.
+//
+// The basic characteristics of Neutron Security Groups are:
+//
+// For ingress traffic (to an instance)
+// - Only traffic matched with security group rules are allowed.
+// - When there is no rule defined, all traffic are dropped.
+//
+// For egress traffic (from an instance)
+// - Only traffic matched with security group rules are allowed.
+// - When there is no rule defined, all egress traffic are dropped.
+// - When a new security group is created, rules to allow all egress traffic
+// are automatically added.
+//
+// "default security group" is defined for each tenant.
+// - For the default security group a rule which allows intercommunication
+// among hosts associated with the default security group is defined by default.
+// - As a result, all egress traffic and intercommunication in the default
+// group are allowed and all ingress from outside of the default group is
+// dropped by default (in the default security group).
+package security
diff --git a/openstack/networking/v2/extensions/security/groups/requests.go b/openstack/networking/v2/extensions/security/groups/requests.go
new file mode 100644
index 0000000..6e9fe33
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/requests.go
@@ -0,0 +1,107 @@
+package groups
+
+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 floating IP 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 {
+ ID string `q:"id"`
+ Name string `q:"name"`
+ 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 groups. 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 SecGroupPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+var (
+ errNameRequired = fmt.Errorf("Name is required")
+)
+
+// CreateOpts contains all the values needed to create a new security group.
+type CreateOpts struct {
+ // Required. Human-readable name for the VIP. Does not have to be unique.
+ Name string
+
+ // Optional. Describes the security group.
+ Description 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.Name == "" {
+ res.Err = errNameRequired
+ return res
+ }
+
+ type secgroup struct {
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ }
+
+ type request struct {
+ SecGroup secgroup `json:"security_group"`
+ }
+
+ reqBody := request{SecGroup: secgroup{
+ Name: opts.Name,
+ Description: opts.Description,
+ }}
+
+ _, 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/groups/requests_test.go b/openstack/networking/v2/extensions/security/groups/requests_test.go
new file mode 100644
index 0000000..b03716d
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/requests_test.go
@@ -0,0 +1,222 @@
+package groups
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+ "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-groups", rootURL(serviceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups/foo", resourceURL(serviceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups", 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_groups": [
+ {
+ "description": "default",
+ "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "name": "default",
+ "security_group_rules": [],
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(serviceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractGroups(page)
+ if err != nil {
+ t.Errorf("Failed to extract secgroups: %v", err)
+ return false, err
+ }
+
+ expected := []SecGroup{
+ SecGroup{
+ Description: "default",
+ ID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ Name: "default",
+ Rules: []rules.SecGroupRule{},
+ 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-groups", 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": {
+ "name": "new-webservers",
+ "description": "security group for webservers"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "security_group": {
+ "description": "security group for webservers",
+ "id": "2076db17-a522-4506-91de-c6dd8e837028",
+ "name": "new-webservers",
+ "security_group_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "565b9502-12de-4ffd-91e9-68885cff6ae1",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ],
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{Name: "new-webservers", Description: "security group for webservers"}
+ _, 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-groups/85cc3048-abc3-43cc-89b3-377341426ac5", 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": {
+ "description": "default",
+ "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "name": "default",
+ "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"
+ }
+ ],
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ sg, err := Get(serviceClient(), "85cc3048-abc3-43cc-89b3-377341426ac5").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "default", sg.Description)
+ th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sg.ID)
+ th.AssertEquals(t, "default", sg.Name)
+ th.AssertEquals(t, 2, len(sg.Rules))
+ th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sg.TenantID)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups/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/groups/results.go b/openstack/networking/v2/extensions/security/groups/results.go
new file mode 100644
index 0000000..6db613e
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/results.go
@@ -0,0 +1,128 @@
+package groups
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SecGroup represents a container for security group rules.
+type SecGroup struct {
+ // The UUID for the security group.
+ ID string
+
+ // Human-readable name for the security group. Might not be unique. Cannot be
+ // named "default" as that is automatically created for a tenant.
+ Name string
+
+ // The security group description.
+ Description string
+
+ // A slice of security group rules that dictate the permitted behaviour for
+ // traffic entering and leaving the group.
+ Rules []rules.SecGroupRule `json:"security_group_rules" mapstructure:"security_group_rules"`
+
+ // Owner of the security group. Only admin users can specify a TenantID
+ // other than their own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// SecGroupPage is the page returned by a pager when traversing over a
+// collection of security groups.
+type SecGroupPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of security groups 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 SecGroupPage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"security_groups_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 SecGroupPage struct is empty.
+func (p SecGroupPage) IsEmpty() (bool, error) {
+ is, err := ExtractGroups(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractGroups accepts a Page struct, specifically a SecGroupPage struct,
+// and extracts the elements into a slice of SecGroup structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractGroups(page pagination.Page) ([]SecGroup, error) {
+ var resp struct {
+ SecGroups []SecGroup `mapstructure:"security_groups" json:"security_groups"`
+ }
+
+ err := mapstructure.Decode(page.(SecGroupPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.SecGroups, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a security group.
+func (r commonResult) Extract() (*SecGroup, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ SecGroup *SecGroup `mapstructure:"security_group" json:"security_group"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron secgroup: %v", err)
+ }
+
+ return res.SecGroup, 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/groups/urls.go b/openstack/networking/v2/extensions/security/groups/urls.go
new file mode 100644
index 0000000..2f2bbdd
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/urls.go
@@ -0,0 +1,16 @@
+package groups
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ rootPath = "security-groups"
+)
+
+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)
+}
diff --git a/openstack/networking/v2/extensions/security/rules/requests.go b/openstack/networking/v2/extensions/security/rules/requests.go
new file mode 100644
index 0000000..1e8c7fa
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/requests.go
@@ -0,0 +1 @@
+package rules
diff --git a/openstack/networking/v2/extensions/security/rules/requests_test.go b/openstack/networking/v2/extensions/security/rules/requests_test.go
new file mode 100644
index 0000000..1e8c7fa
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/requests_test.go
@@ -0,0 +1 @@
+package rules
diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go
new file mode 100644
index 0000000..8e948e7
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -0,0 +1,3 @@
+package rules
+
+type SecGroupRule struct{}
diff --git a/openstack/networking/v2/extensions/security/rules/urls.go b/openstack/networking/v2/extensions/security/rules/urls.go
new file mode 100644
index 0000000..1e8c7fa
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/urls.go
@@ -0,0 +1 @@
+package rules