Add rules support
diff --git a/openstack/compute/v2/extensions/secgroups/fixtures.go b/openstack/compute/v2/extensions/secgroups/fixtures.go
index 55c7e21..519bc74 100644
--- a/openstack/compute/v2/extensions/secgroups/fixtures.go
+++ b/openstack/compute/v2/extensions/secgroups/fixtures.go
@@ -128,3 +128,49 @@
 		w.WriteHeader(http.StatusAccepted)
 	})
 }
+
+func mockAddRuleResponse(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": 22,
+    "ip_protocol": "TCP",
+    "to_port": 22,
+    "parent_group_id": "b0e0d7dd-2ca4-49a9-ba82-c44a148b66a5",
+    "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": 22,
+    "group": {},
+    "ip_protocol": "TCP",
+    "to_port": 22,
+    "parent_group_id": "b0e0d7dd-2ca4-49a9-ba82-c44a148b66a5",
+    "ip_range": {
+      "cidr": "0.0.0.0/0"
+    },
+    "id": "f9a97fcf-3a97-47b0-b76f-919136afb7ed"
+  }
+}`)
+	})
+}
+
+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) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
diff --git a/openstack/compute/v2/extensions/secgroups/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go
index 2d9dbe4..983f734 100644
--- a/openstack/compute/v2/extensions/secgroups/requests.go
+++ b/openstack/compute/v2/extensions/secgroups/requests.go
@@ -1,6 +1,8 @@
 package secgroups
 
 import (
+	"errors"
+
 	"github.com/racker/perigee"
 
 	"github.com/rackspace/gophercloud"
@@ -72,3 +74,66 @@
 
 	return result
 }
+
+type AddRuleOpts struct {
+	// Required - the ID of the group that this rule will be added to.
+	ParentGroupID string `json:"parent_group_id"`
+
+	// Required - the lower bound of the port range that will be opened.
+	FromPort int `json:"from_port"`
+
+	// Required - the upper bound of the port range that will be opened.
+	ToPort int `json:"to_port"`
+
+	// Required - the protocol type that will be allowed, e.g. TCP.
+	IPProtocol string `json:"ip_protocol"`
+
+	// 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"`
+
+	// 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"`
+}
+
+func AddRule(client *gophercloud.ServiceClient, opts AddRuleOpts) AddRuleResult {
+	var result AddRuleResult
+
+	if opts.ParentGroupID == "" {
+		result.Err = errors.New("A ParentGroupID must be set")
+		return result
+	}
+	if opts.FromPort == 0 {
+		result.Err = errors.New("A FromPort must be set")
+		return result
+	}
+	if opts.ToPort == 0 {
+		result.Err = errors.New("A ToPort must be set")
+		return result
+	}
+	if opts.IPProtocol == "" {
+		result.Err = errors.New("A IPProtocol must be set")
+		return result
+	}
+	if opts.CIDR == "" && opts.FromGroupID == "" {
+		result.Err = errors.New("A CIDR or FromGroupID must be set")
+		return result
+	}
+
+	reqBody := struct {
+		AddRuleOpts `json:"security_group_rule"`
+	}{opts}
+
+	_, result.Err = perigee.Request("POST", rootRuleURL(client), perigee.Options{
+		Results:     &result.Body,
+		ReqBody:     &reqBody,
+		MoreHeaders: client.AuthenticatedHeaders(),
+		OkCodes:     []int{200},
+	})
+
+	return result
+}
diff --git a/openstack/compute/v2/extensions/secgroups/requests_test.go b/openstack/compute/v2/extensions/secgroups/requests_test.go
index 1dc43d3..f59471f 100644
--- a/openstack/compute/v2/extensions/secgroups/requests_test.go
+++ b/openstack/compute/v2/extensions/secgroups/requests_test.go
@@ -11,6 +11,7 @@
 const (
 	serverID = "{serverID}"
 	groupID  = "b0e0d7dd-2ca4-49a9-ba82-c44a148b66a5"
+	ruleID   = "a4070a0f-5383-454c-872d-58c034bc981b"
 )
 
 func TestList(t *testing.T) {
@@ -146,3 +147,33 @@
 	err := Delete(client.ServiceClient(), groupID).ExtractErr()
 	th.AssertNoErr(t, err)
 }
+
+func TestAddRule(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockAddRuleResponse(t)
+
+	opts := AddRuleOpts{
+		ParentGroupID: "b0e0d7dd-2ca4-49a9-ba82-c44a148b66a5",
+		FromPort:      22,
+		ToPort:        22,
+		IPProtocol:    "TCP",
+		CIDR:          "0.0.0.0/0",
+	}
+
+	rule, err := AddRule(client.ServiceClient(), opts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := &Rule{
+		FromPort:      22,
+		ToPort:        22,
+		Group:         Group{},
+		IPProtocol:    "TCP",
+		ParentGroupID: "b0e0d7dd-2ca4-49a9-ba82-c44a148b66a5",
+		IPRange:       IPRange{CIDR: "0.0.0.0/0"},
+		ID:            "f9a97fcf-3a97-47b0-b76f-919136afb7ed",
+	}
+
+	th.AssertDeepEquals(t, expected, rule)
+}
diff --git a/openstack/compute/v2/extensions/secgroups/results.go b/openstack/compute/v2/extensions/secgroups/results.go
index 6418970..c814048 100644
--- a/openstack/compute/v2/extensions/secgroups/results.go
+++ b/openstack/compute/v2/extensions/secgroups/results.go
@@ -84,3 +84,21 @@
 
 	return &response.SecurityGroup, err
 }
+
+type AddRuleResult struct {
+	gophercloud.Result
+}
+
+func (r AddRuleResult) Extract() (*Rule, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Rule Rule `mapstructure:"security_group_rule"`
+	}
+
+	err := mapstructure.Decode(r.Body, &response)
+
+	return &response.Rule, err
+}
diff --git a/openstack/compute/v2/extensions/secgroups/urls.go b/openstack/compute/v2/extensions/secgroups/urls.go
index 9c1859f..37a9048 100644
--- a/openstack/compute/v2/extensions/secgroups/urls.go
+++ b/openstack/compute/v2/extensions/secgroups/urls.go
@@ -4,6 +4,7 @@
 
 const (
 	secgrouppath = "os-security-groups"
+	rulepath     = "os-security-group-rules"
 )
 
 func resourceURL(c *gophercloud.ServiceClient, id string) string {
@@ -17,3 +18,7 @@
 func listByServerURL(c *gophercloud.ServiceClient, serverID string) string {
 	return c.ServiceURL(secgrouppath, "servers", serverID, secgrouppath)
 }
+
+func rootRuleURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rulepath)
+}