create flavor (#257)

* create flavor operation

* accept 200,201 status codes

* unit test
diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go
index ef133ff..d5d571c 100644
--- a/openstack/compute/v2/flavors/requests.go
+++ b/openstack/compute/v2/flavors/requests.go
@@ -54,6 +54,47 @@
 	})
 }
 
+type CreateOptsBuilder interface {
+	ToFlavorCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is passed to Create to create a flavor
+// Source:
+// https://github.com/openstack/nova/blob/stable/newton/nova/api/openstack/compute/schemas/flavor_manage.py#L20
+type CreateOpts struct {
+	Name string `json:"name" required:"true"`
+	// memory size, in MBs
+	RAM   int `json:"ram" required:"true"`
+	VCPUs int `json:"vcpus" required:"true"`
+	// disk size, in GBs
+	Disk *int   `json:"disk" required:"true"`
+	ID   string `json:"id,omitempty"`
+	// non-zero, positive
+	Swap       *int    `json:"swap,omitempty"`
+	RxTxFactor float64 `json:"rxtx_factor,omitempty"`
+	IsPublic   *bool   `json:"os-flavor-access:is_public,omitempty"`
+	// ephemeral disk size, in GBs, non-zero, positive
+	Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"`
+}
+
+// ToFlavorCreateMap satisfies the CreateOptsBuilder interface
+func (opts *CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) {
+	return gophercloud.BuildRequestBody(opts, "flavor")
+}
+
+// Create a flavor
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	b, err := opts.ToFlavorCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+	_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200, 201},
+	})
+	return
+}
+
 // Get instructs OpenStack to provide details on a single flavor, identified by its ID.
 // Use ExtractFlavor to convert its result into a Flavor.
 func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go
index a49de0d..18b8434 100644
--- a/openstack/compute/v2/flavors/results.go
+++ b/openstack/compute/v2/flavors/results.go
@@ -8,13 +8,21 @@
 	"github.com/gophercloud/gophercloud/pagination"
 )
 
-// GetResult temporarily holds the response from a Get call.
-type GetResult struct {
+type commonResult struct {
 	gophercloud.Result
 }
 
-// Extract provides access to the individual Flavor returned by the Get function.
-func (r GetResult) Extract() (*Flavor, error) {
+type CreateResult struct {
+	commonResult
+}
+
+// GetResult temporarily holds the response from a Get call.
+type GetResult struct {
+	commonResult
+}
+
+// Extract provides access to the individual Flavor returned by the Get and Create functions.
+func (r commonResult) Extract() (*Flavor, error) {
 	var s struct {
 		Flavor *Flavor `json:"flavor"`
 	}
@@ -40,41 +48,32 @@
 	VCPUs int `json:"vcpus"`
 }
 
-func (f *Flavor) UnmarshalJSON(b []byte) error {
-	var flavor struct {
-		ID         string      `json:"id"`
-		Disk       int         `json:"disk"`
-		RAM        int         `json:"ram"`
-		Name       string      `json:"name"`
-		RxTxFactor float64     `json:"rxtx_factor"`
-		Swap       interface{} `json:"swap"`
-		VCPUs      int         `json:"vcpus"`
+func (r *Flavor) UnmarshalJSON(b []byte) error {
+	type tmp Flavor
+	var s struct {
+		tmp
+		Swap interface{} `json:"swap"`
 	}
-	err := json.Unmarshal(b, &flavor)
+	err := json.Unmarshal(b, &s)
 	if err != nil {
 		return err
 	}
 
-	f.ID = flavor.ID
-	f.Disk = flavor.Disk
-	f.RAM = flavor.RAM
-	f.Name = flavor.Name
-	f.RxTxFactor = flavor.RxTxFactor
-	f.VCPUs = flavor.VCPUs
+	*r = Flavor(s.tmp)
 
-	switch t := flavor.Swap.(type) {
+	switch t := s.Swap.(type) {
 	case float64:
-		f.Swap = int(t)
+		r.Swap = int(t)
 	case string:
 		switch t {
 		case "":
-			f.Swap = 0
+			r.Swap = 0
 		default:
 			swap, err := strconv.ParseFloat(t, 64)
 			if err != nil {
 				return err
 			}
-			f.Swap = int(swap)
+			r.Swap = int(swap)
 		}
 	}
 
diff --git a/openstack/compute/v2/flavors/testing/requests_test.go b/openstack/compute/v2/flavors/testing/requests_test.go
index 1b96933..f4f1c8d 100644
--- a/openstack/compute/v2/flavors/testing/requests_test.go
+++ b/openstack/compute/v2/flavors/testing/requests_test.go
@@ -132,3 +132,55 @@
 		t.Errorf("Expected %#v, but was %#v", expected, actual)
 	}
 }
+
+func TestCreateFlavor(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/flavors", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+				"flavor": {
+					"id": "1",
+					"name": "m1.tiny",
+					"disk": 1,
+					"ram": 512,
+					"vcpus": 1,
+					"rxtx_factor": 1,
+					"swap": ""
+				}
+			}
+		`)
+	})
+
+	disk := 1
+	opts := &flavors.CreateOpts{
+		ID:         "1",
+		Name:       "m1.tiny",
+		Disk:       &disk,
+		RAM:        512,
+		VCPUs:      1,
+		RxTxFactor: 1.0,
+	}
+	actual, err := flavors.Create(fake.ServiceClient(), opts).Extract()
+	if err != nil {
+		t.Fatalf("Unable to create flavor: %v", err)
+	}
+
+	expected := &flavors.Flavor{
+		ID:         "1",
+		Name:       "m1.tiny",
+		Disk:       1,
+		RAM:        512,
+		VCPUs:      1,
+		RxTxFactor: 1,
+		Swap:       0,
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("Expected %#v, but was %#v", expected, actual)
+	}
+}
diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go
index ee0dfdb..2fc2179 100644
--- a/openstack/compute/v2/flavors/urls.go
+++ b/openstack/compute/v2/flavors/urls.go
@@ -11,3 +11,7 @@
 func listURL(client *gophercloud.ServiceClient) string {
 	return client.ServiceURL("flavors", "detail")
 }
+
+func createURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("flavors")
+}