Allow Any Protocol in FWaaS Rules (#162)

This commit enables FWaaS Rules to allow any protocols. It does this
by creating constants for each valid type of protocol, and upon the
type of ProtocolAny being used, the request's protocol parameter will
be null.
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go b/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go
index 64ce402..6533a51 100644
--- a/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go
@@ -78,7 +78,7 @@
 
 	createOpts := rules.CreateOpts{
 		Name:                 ruleName,
-		Protocol:             "tcp",
+		Protocol:             rules.ProtocolTCP,
 		Action:               "allow",
 		SourceIPAddress:      sourceAddress,
 		SourcePort:           sourcePort,
diff --git a/openstack/networking/v2/extensions/fwaas/rules/requests.go b/openstack/networking/v2/extensions/fwaas/rules/requests.go
index 6b0814c..c1784b7 100644
--- a/openstack/networking/v2/extensions/fwaas/rules/requests.go
+++ b/openstack/networking/v2/extensions/fwaas/rules/requests.go
@@ -5,6 +5,25 @@
 	"github.com/gophercloud/gophercloud/pagination"
 )
 
+type (
+	// Protocol represents a valid rule protocol
+	Protocol string
+)
+
+const (
+	// ProtocolAny is to allow any protocol
+	ProtocolAny Protocol = "any"
+
+	// ProtocolICMP is to allow the ICMP protocol
+	ProtocolICMP Protocol = "icmp"
+
+	// ProtocolTCP is to allow the TCP protocol
+	ProtocolTCP Protocol = "tcp"
+
+	// ProtocolUDP is to allow the UDP protocol
+	ProtocolUDP Protocol = "udp"
+)
+
 // ListOptsBuilder allows extensions to add additional parameters to the
 // List request.
 type ListOptsBuilder interface {
@@ -76,7 +95,7 @@
 
 // CreateOpts contains all the values needed to create a new firewall rule.
 type CreateOpts struct {
-	Protocol             string                `json:"protocol" required:"true"`
+	Protocol             Protocol              `json:"protocol" required:"true"`
 	Action               string                `json:"action" required:"true"`
 	TenantID             string                `json:"tenant_id,omitempty"`
 	Name                 string                `json:"name,omitempty"`
@@ -92,7 +111,16 @@
 
 // ToRuleCreateMap casts a CreateOpts struct to a map.
 func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
-	return gophercloud.BuildRequestBody(opts, "firewall_rule")
+	b, err := gophercloud.BuildRequestBody(opts, "firewall_rule")
+	if err != nil {
+		return nil, err
+	}
+
+	if m := b["firewall_rule"].(map[string]interface{}); m["protocol"] == "any" {
+		m["protocol"] = nil
+	}
+
+	return b, nil
 }
 
 // Create accepts a CreateOpts struct and uses the values to create a new firewall rule
diff --git a/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go b/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go
index dbf377a..2fedfa8 100644
--- a/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go
+++ b/openstack/networking/v2/extensions/fwaas/rules/testing/requests_test.go
@@ -173,7 +173,7 @@
 
 	options := rules.CreateOpts{
 		TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
-		Protocol:             "tcp",
+		Protocol:             rules.ProtocolTCP,
 		Description:          "ssh rule",
 		DestinationIPAddress: "192.168.1.0/24",
 		DestinationPort:      "22",
@@ -185,6 +185,67 @@
 	th.AssertNoErr(t, err)
 }
 
+func TestCreateAnyProtocol(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_rules", 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, `
+{
+	"firewall_rule": {
+		"protocol": null,
+		"description": "any to 192.168.1.0/24",
+		"destination_ip_address": "192.168.1.0/24",
+		"name": "any_to_192.168.1.0/24",
+		"action": "allow",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61"
+	}
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+	"firewall_rule":{
+		"protocol": null,
+		"description": "any to 192.168.1.0/24",
+		"source_port": null,
+		"source_ip_address": null,
+		"destination_ip_address": "192.168.1.0/24",
+		"firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+		"position": 2,
+		"destination_port": null,
+		"id": "f03bd950-6c56-4f5e-a307-45967078f507",
+		"name": "any_to_192.168.1.0/24",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+		"enabled": true,
+		"action": "allow",
+		"ip_version": 4,
+		"shared": false
+	}
+}
+        `)
+	})
+
+	options := rules.CreateOpts{
+		TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
+		Protocol:             rules.ProtocolAny,
+		Description:          "any to 192.168.1.0/24",
+		DestinationIPAddress: "192.168.1.0/24",
+		Name:                 "any_to_192.168.1.0/24",
+		Action:               "allow",
+	}
+
+	_, err := rules.Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+}
+
 func TestGet(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()