Merge pull request #554 from bison/autoscale-policies

[RFR] Rackspace Auto Scale: policies
diff --git a/rackspace/autoscale/v1/policies/doc.go b/rackspace/autoscale/v1/policies/doc.go
new file mode 100644
index 0000000..c4d3bab
--- /dev/null
+++ b/rackspace/autoscale/v1/policies/doc.go
@@ -0,0 +1,9 @@
+/*
+Package policies provides information and interaction with the policy API
+resource in the Rackspace Auto Scale service.
+
+Auto Scale uses policies to define when and how scaling activity will take
+place.  Scaling policies specify how to modify the scaling group and its
+behavior.  You can specify multiple policies to manage a scaling group.
+*/
+package policies
diff --git a/rackspace/autoscale/v1/policies/fixtures.go b/rackspace/autoscale/v1/policies/fixtures.go
new file mode 100644
index 0000000..9334740
--- /dev/null
+++ b/rackspace/autoscale/v1/policies/fixtures.go
@@ -0,0 +1,259 @@
+// +build fixtures
+
+package policies
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// PolicyListBody contains the canned body of a policies.List response.
+const PolicyListBody = `
+{
+  "policies_links": [],
+  "policies": [
+    {
+      "name": "webhook policy",
+      "links": [
+        {
+          "href": "https://dfw.autoscale.api.rackspacecloud.com/v1.0/123456/groups/60b15dad-5ea1-43fa-9a12-a1d737b4da07/policies/2b48d247-0282-4b9d-8775-5c4b67e8e649/",
+          "rel": "self"
+        }
+      ],
+      "changePercent": 3.3,
+      "cooldown": 300,
+      "type": "webhook",
+      "id": "2b48d247-0282-4b9d-8775-5c4b67e8e649"
+    },
+    {
+      "cooldown": 0,
+      "name": "one time",
+      "links": [
+        {
+          "href": "https://dfw.autoscale.api.rackspacecloud.com/v1.0/123456/groups/60b15dad-5ea1-43fa-9a12-a1d737b4da07/policies/c175c31e-65f9-41de-8b15-918420d3253e/",
+          "rel": "self"
+        }
+      ],
+      "args": {
+        "at": "2020-04-01T23:00:00.000Z"
+      },
+      "type": "schedule",
+      "id": "c175c31e-65f9-41de-8b15-918420d3253e",
+      "change": -1
+    },
+    {
+      "cooldown": 0,
+      "name": "sunday afternoon",
+      "links": [
+        {
+          "href": "https://dfw.autoscale.api.rackspacecloud.com/v1.0/123456/groups/60b15dad-5ea1-43fa-9a12-a1d737b4da07/policies/e785e3e7-af9e-4f3c-99ae-b80a532e1663/",
+          "rel": "self"
+        }
+      ],
+      "args": {
+        "cron": "59 15 * * 0"
+      },
+      "type": "schedule",
+      "id": "e785e3e7-af9e-4f3c-99ae-b80a532e1663",
+      "desiredCapacity": 2
+    }
+  ]
+}
+`
+
+// PolicyCreateBody contains the canned body of a policies.Create response.
+const PolicyCreateBody = PolicyListBody
+
+// PolicyCreateRequest contains the canned body of a policies.Create request.
+const PolicyCreateRequest = `
+[
+  {
+    "name": "webhook policy",
+    "changePercent": 3.3,
+    "cooldown": 300,
+    "type": "webhook"
+  },
+  {
+    "cooldown": 0,
+    "name": "one time",
+    "args": {
+      "at": "2020-04-01T23:00:00Z"
+    },
+    "type": "schedule",
+    "change": -1
+  },
+  {
+    "cooldown": 0,
+    "name": "sunday afternoon",
+    "args": {
+      "cron": "59 15 * * 0"
+    },
+    "type": "schedule",
+    "desiredCapacity": 2
+  }
+]
+`
+
+// PolicyGetBody contains the canned body of a policies.Get response.
+const PolicyGetBody = `
+{
+  "policy": {
+    "name": "webhook policy",
+    "links": [
+      {
+        "href": "https://dfw.autoscale.api.rackspacecloud.com/v1.0/123456/groups/60b15dad-5ea1-43fa-9a12-a1d737b4da07/policies/2b48d247-0282-4b9d-8775-5c4b67e8e649/",
+        "rel": "self"
+      }
+    ],
+    "changePercent": 3.3,
+    "cooldown": 300,
+    "type": "webhook",
+    "id": "2b48d247-0282-4b9d-8775-5c4b67e8e649"
+  }
+}
+`
+
+// PolicyUpdateRequest contains the canned body of a policies.Update request.
+const PolicyUpdateRequest = `
+{
+  "name": "updated webhook policy",
+  "type": "webhook",
+  "cooldown": 600,
+  "changePercent": 6.6
+}
+`
+
+var (
+	// WebhookPolicy is a Policy corresponding to the first result in PolicyListBody.
+	WebhookPolicy = Policy{
+		ID:              "2b48d247-0282-4b9d-8775-5c4b67e8e649",
+		Name:            "webhook policy",
+		Type:            Webhook,
+		Cooldown:        300,
+		AdjustmentType:  ChangePercent,
+		AdjustmentValue: 3.3,
+	}
+
+	// OneTimePolicy is a Policy corresponding to the second result in PolicyListBody.
+	OneTimePolicy = Policy{
+		ID:              "c175c31e-65f9-41de-8b15-918420d3253e",
+		Name:            "one time",
+		Type:            Schedule,
+		AdjustmentType:  Change,
+		AdjustmentValue: float64(-1),
+		Schedule:        At(time.Date(2020, time.April, 01, 23, 0, 0, 0, time.UTC)),
+	}
+
+	// SundayAfternoonPolicy is a Policy corresponding to the third result in PolicyListBody.
+	SundayAfternoonPolicy = Policy{
+		ID:              "e785e3e7-af9e-4f3c-99ae-b80a532e1663",
+		Name:            "sunday afternoon",
+		Type:            Schedule,
+		AdjustmentType:  DesiredCapacity,
+		AdjustmentValue: float64(2),
+		Schedule:        Cron("59 15 * * 0"),
+	}
+)
+
+// HandlePolicyListSuccessfully sets up the test server to respond to a policies List request.
+func HandlePolicyListSuccessfully(t *testing.T) {
+	path := "/groups/60b15dad-5ea1-43fa-9a12-a1d737b4da07/policies"
+
+	th.Mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+
+		fmt.Fprintf(w, PolicyListBody)
+	})
+}
+
+// HandlePolicyCreateSuccessfully sets up the test server to respond to a policies Create request.
+func HandlePolicyCreateSuccessfully(t *testing.T) {
+	path := "/groups/60b15dad-5ea1-43fa-9a12-a1d737b4da07/policies"
+
+	th.Mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+
+		th.TestJSONRequest(t, r, PolicyCreateRequest)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, PolicyCreateBody)
+	})
+}
+
+// HandlePolicyGetSuccessfully sets up the test server to respond to a policies Get request.
+func HandlePolicyGetSuccessfully(t *testing.T) {
+	groupID := "60b15dad-5ea1-43fa-9a12-a1d737b4da07"
+	policyID := "2b48d247-0282-4b9d-8775-5c4b67e8e649"
+
+	path := fmt.Sprintf("/groups/%s/policies/%s", groupID, policyID)
+
+	th.Mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+
+		fmt.Fprintf(w, PolicyGetBody)
+	})
+}
+
+// HandlePolicyUpdateSuccessfully sets up the test server to respond to a policies Update request.
+func HandlePolicyUpdateSuccessfully(t *testing.T) {
+	groupID := "60b15dad-5ea1-43fa-9a12-a1d737b4da07"
+	policyID := "2b48d247-0282-4b9d-8775-5c4b67e8e649"
+
+	path := fmt.Sprintf("/groups/%s/policies/%s", groupID, policyID)
+
+	th.Mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		th.TestJSONRequest(t, r, PolicyUpdateRequest)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandlePolicyDeleteSuccessfully sets up the test server to respond to a policies Delete request.
+func HandlePolicyDeleteSuccessfully(t *testing.T) {
+	groupID := "60b15dad-5ea1-43fa-9a12-a1d737b4da07"
+	policyID := "2b48d247-0282-4b9d-8775-5c4b67e8e649"
+
+	path := fmt.Sprintf("/groups/%s/policies/%s", groupID, policyID)
+
+	th.Mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandlePolicyExecuteSuccessfully sets up the test server to respond to a policies Execute request.
+func HandlePolicyExecuteSuccessfully(t *testing.T) {
+	groupID := "60b15dad-5ea1-43fa-9a12-a1d737b4da07"
+	policyID := "2b48d247-0282-4b9d-8775-5c4b67e8e649"
+
+	path := fmt.Sprintf("/groups/%s/policies/%s/execute", groupID, policyID)
+
+	th.Mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.WriteHeader(http.StatusAccepted)
+		fmt.Fprintf(w, "{}")
+	})
+}
diff --git a/rackspace/autoscale/v1/policies/requests.go b/rackspace/autoscale/v1/policies/requests.go
new file mode 100644
index 0000000..7aadf98
--- /dev/null
+++ b/rackspace/autoscale/v1/policies/requests.go
@@ -0,0 +1,302 @@
+package policies
+
+import (
+	"errors"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Validation errors returned by create or update operations.
+var (
+	ErrNoName            = errors.New("Policy name cannot be empty.")
+	ErrNoSchedule        = errors.New("Schedule cannot be nil for schedule policies.")
+	ErrCooldownRange     = errors.New("Cooldown is out of range (0, 86400).")
+	ErrUnknownType       = errors.New("Unknown policy type.")
+	ErrUnknownAdjustment = errors.New("Unknown adjustment type.")
+	ErrEmptyCron         = errors.New("Cron argument cannot be empty.")
+)
+
+// List returns all scaling policies for a group.
+func List(client *gophercloud.ServiceClient, groupID string) pagination.Pager {
+	url := listURL(client, groupID)
+
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return PolicyPage{pagination.SinglePageBase(r)}
+	}
+
+	return pagination.NewPager(client, url, createPageFn)
+}
+
+// CreateOptsBuilder is the interface responsible for generating the map that
+// will be marshalled to JSON for a Create operation.
+type CreateOptsBuilder interface {
+	ToPolicyCreateMap() ([]map[string]interface{}, error)
+}
+
+// CreateOpts is a slice of CreateOpt structs that allow the user to create
+// multiple policies in a single operation.
+type CreateOpts []CreateOpt
+
+// CreateOpt represents the options to create a policy.
+type CreateOpt struct {
+	// Name [required] is a name for the policy.
+	Name string
+
+	// Type [required] of policy, i.e. either "webhook" or "schedule".
+	Type Type
+
+	// Cooldown [required] period in seconds.
+	Cooldown int
+
+	// AdjustmentType [requried] is the method used to change the capacity of
+	// the group, i.e. one of: Change, ChangePercent, or DesiredCapacity.
+	AdjustmentType AdjustmentType
+
+	// AdjustmentValue [required] is the numeric value of the adjustment.  For
+	// adjustments of type Change or DesiredCapacity, this will be converted to
+	// an integer.
+	AdjustmentValue float64
+
+	// Value determining Schedule policy behavior, or nil for Webhook policies.
+	// This should be an appropriately configured Cron or an At value.
+	Schedule ScheduleArgs
+}
+
+// ToPolicyCreateMap converts a slice of CreateOpt structs into a map for use
+// in the request body of a Create operation.
+func (opts CreateOpts) ToPolicyCreateMap() ([]map[string]interface{}, error) {
+	var policies []map[string]interface{}
+
+	for _, o := range opts {
+		if o.Name == "" {
+			return nil, ErrNoName
+		}
+
+		if o.Type == Schedule && o.Schedule == nil {
+			return nil, ErrNoSchedule
+		}
+
+		if ok := validateType(o.Type); !ok {
+			return nil, ErrUnknownType
+		}
+
+		if ok := validateCooldown(o.Cooldown); !ok {
+			return nil, ErrCooldownRange
+		}
+
+		policy := make(map[string]interface{})
+
+		policy["name"] = o.Name
+		policy["type"] = o.Type
+		policy["cooldown"] = o.Cooldown
+
+		err := setAdjustment(o.AdjustmentType, o.AdjustmentValue, policy)
+
+		if err != nil {
+			return nil, err
+		}
+
+		if o.Schedule != nil {
+			args, err := o.Schedule.ToPolicyArgs()
+
+			if err != nil {
+				return nil, err
+			}
+
+			policy["args"] = args
+		}
+
+		policies = append(policies, policy)
+	}
+
+	return policies, nil
+}
+
+// Create requests a new policy be created and associated with the given group.
+func Create(client *gophercloud.ServiceClient, groupID string, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToPolicyCreateMap()
+
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = client.Post(createURL(client, groupID), reqBody, &res.Body, nil)
+
+	return res
+}
+
+// Get requests the details of a single policy with the given ID.
+func Get(client *gophercloud.ServiceClient, groupID, policyID string) GetResult {
+	var result GetResult
+
+	_, result.Err = client.Get(getURL(client, groupID, policyID), &result.Body, nil)
+
+	return result
+}
+
+// UpdateOptsBuilder is the interface responsible for generating the map
+// structure for producing JSON for an Update operation.
+type UpdateOptsBuilder interface {
+	ToPolicyUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents the options for updating an existing policy.
+//
+// Update operations completely replace the configuration being updated. Empty
+// values in the update are accepted and overwrite previously specified
+// parameters.
+type UpdateOpts struct {
+	// Name [required] is a name for the policy.
+	Name string
+
+	// Type [required] of policy, i.e. either "webhook" or "schedule".
+	Type Type
+
+	// Cooldown [required] period in seconds.  If you don't specify a cooldown,
+	// it will default to zero, and the policy will be configured as such.
+	Cooldown int
+
+	// AdjustmentType [requried] is the method used to change the capacity of
+	// the group, i.e. one of: Change, ChangePercent, or DesiredCapacity.
+	AdjustmentType AdjustmentType
+
+	// AdjustmentValue [required] is the numeric value of the adjustment.  For
+	// adjustments of type Change or DesiredCapacity, this will be converted to
+	// an integer.
+	AdjustmentValue float64
+
+	// Value determining Schedule policy behavior, or nil for Webhook policies.
+	// This should be an appropriately configured Cron or an At value.
+	Schedule ScheduleArgs
+}
+
+// ToPolicyUpdateMap converts an UpdateOpts struct into a map for use as the
+// request body in an Update request.
+func (opts UpdateOpts) ToPolicyUpdateMap() (map[string]interface{}, error) {
+	if opts.Name == "" {
+		return nil, ErrNoName
+	}
+
+	if opts.Type == Schedule && opts.Schedule == nil {
+		return nil, ErrNoSchedule
+	}
+
+	if ok := validateType(opts.Type); !ok {
+		return nil, ErrUnknownType
+	}
+
+	if ok := validateCooldown(opts.Cooldown); !ok {
+		return nil, ErrCooldownRange
+	}
+
+	policy := make(map[string]interface{})
+
+	policy["name"] = opts.Name
+	policy["type"] = opts.Type
+	policy["cooldown"] = opts.Cooldown
+
+	err := setAdjustment(opts.AdjustmentType, opts.AdjustmentValue, policy)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if opts.Schedule != nil {
+		args, err := opts.Schedule.ToPolicyArgs()
+
+		if err != nil {
+			return nil, err
+		}
+
+		policy["args"] = args
+	}
+
+	return policy, nil
+}
+
+// Update requests the configuration of the given policy be updated.
+func Update(client *gophercloud.ServiceClient, groupID, policyID string, opts UpdateOptsBuilder) UpdateResult {
+	var result UpdateResult
+
+	url := updateURL(client, groupID, policyID)
+	reqBody, err := opts.ToPolicyUpdateMap()
+
+	if err != nil {
+		result.Err = err
+		return result
+	}
+
+	_, result.Err = client.Put(url, reqBody, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+
+	return result
+}
+
+// Delete requests the given policy be permanently deleted.
+func Delete(client *gophercloud.ServiceClient, groupID, policyID string) DeleteResult {
+	var result DeleteResult
+
+	url := deleteURL(client, groupID, policyID)
+	_, result.Err = client.Delete(url, &gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+
+	return result
+}
+
+// Execute requests the given policy be executed immediately.
+func Execute(client *gophercloud.ServiceClient, groupID, policyID string) ExecuteResult {
+	var result ExecuteResult
+
+	url := executeURL(client, groupID, policyID)
+	_, result.Err = client.Post(url, nil, &result.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{202},
+	})
+
+	return result
+}
+
+// Validate and set an adjustment on the given request body.
+func setAdjustment(t AdjustmentType, v float64, body map[string]interface{}) error {
+	key := string(t)
+
+	switch t {
+	case ChangePercent:
+		body[key] = v
+
+	case Change, DesiredCapacity:
+		body[key] = int(v)
+
+	default:
+		return ErrUnknownAdjustment
+	}
+
+	return nil
+}
+
+func validateType(t Type) (ok bool) {
+	switch t {
+	case Schedule, Webhook:
+		ok = true
+		return
+
+	default:
+		ok = false
+		return
+	}
+}
+
+func validateCooldown(cooldown int) (ok bool) {
+	if cooldown < 0 || cooldown > 86400 {
+		ok = false
+		return
+	}
+
+	ok = true
+	return
+}
diff --git a/rackspace/autoscale/v1/policies/requests_test.go b/rackspace/autoscale/v1/policies/requests_test.go
new file mode 100644
index 0000000..88a52a7
--- /dev/null
+++ b/rackspace/autoscale/v1/policies/requests_test.go
@@ -0,0 +1,168 @@
+package policies
+
+import (
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+	groupID         = "60b15dad-5ea1-43fa-9a12-a1d737b4da07"
+	webhookPolicyID = "2b48d247-0282-4b9d-8775-5c4b67e8e649"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePolicyListSuccessfully(t)
+
+	pages := 0
+	pager := List(client.ServiceClient(), "60b15dad-5ea1-43fa-9a12-a1d737b4da07")
+
+	err := pager.EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		policies, err := ExtractPolicies(page)
+
+		if err != nil {
+			return false, err
+		}
+
+		if len(policies) != 3 {
+			t.Fatalf("Expected 3 policies, got %d", len(policies))
+		}
+
+		th.CheckDeepEquals(t, WebhookPolicy, policies[0])
+		th.CheckDeepEquals(t, OneTimePolicy, policies[1])
+		th.CheckDeepEquals(t, SundayAfternoonPolicy, policies[2])
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePolicyCreateSuccessfully(t)
+
+	oneTime := time.Date(2020, time.April, 01, 23, 0, 0, 0, time.UTC)
+	client := client.ServiceClient()
+	opts := CreateOpts{
+		{
+			Name:            "webhook policy",
+			Type:            Webhook,
+			Cooldown:        300,
+			AdjustmentType:  ChangePercent,
+			AdjustmentValue: 3.3,
+		},
+		{
+			Name:            "one time",
+			Type:            Schedule,
+			AdjustmentType:  Change,
+			AdjustmentValue: -1,
+			Schedule:        At(oneTime),
+		},
+		{
+			Name:            "sunday afternoon",
+			Type:            Schedule,
+			AdjustmentType:  DesiredCapacity,
+			AdjustmentValue: 2,
+			Schedule:        Cron("59 15 * * 0"),
+		},
+	}
+
+	policies, err := Create(client, groupID, opts).Extract()
+
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, WebhookPolicy, policies[0])
+	th.CheckDeepEquals(t, OneTimePolicy, policies[1])
+	th.CheckDeepEquals(t, SundayAfternoonPolicy, policies[2])
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePolicyGetSuccessfully(t)
+
+	client := client.ServiceClient()
+
+	policy, err := Get(client, groupID, webhookPolicyID).Extract()
+
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, WebhookPolicy, *policy)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePolicyUpdateSuccessfully(t)
+
+	client := client.ServiceClient()
+	opts := UpdateOpts{
+		Name:            "updated webhook policy",
+		Type:            Webhook,
+		Cooldown:        600,
+		AdjustmentType:  ChangePercent,
+		AdjustmentValue: 6.6,
+	}
+
+	err := Update(client, groupID, webhookPolicyID, opts).ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePolicyDeleteSuccessfully(t)
+
+	client := client.ServiceClient()
+	err := Delete(client, groupID, webhookPolicyID).ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestExecute(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePolicyExecuteSuccessfully(t)
+
+	client := client.ServiceClient()
+	err := Execute(client, groupID, webhookPolicyID).ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func TestValidateType(t *testing.T) {
+	ok := validateType(Schedule)
+	th.AssertEquals(t, true, ok)
+
+	ok = validateType(Webhook)
+	th.AssertEquals(t, true, ok)
+
+	ok = validateType("BAD")
+	th.AssertEquals(t, false, ok)
+}
+
+func TestValidateCooldown(t *testing.T) {
+	ok := validateCooldown(0)
+	th.AssertEquals(t, true, ok)
+
+	ok = validateCooldown(86400)
+	th.AssertEquals(t, true, ok)
+
+	ok = validateCooldown(-1)
+	th.AssertEquals(t, false, ok)
+
+	ok = validateCooldown(172800)
+	th.AssertEquals(t, false, ok)
+}
diff --git a/rackspace/autoscale/v1/policies/results.go b/rackspace/autoscale/v1/policies/results.go
new file mode 100644
index 0000000..99d8b1a
--- /dev/null
+++ b/rackspace/autoscale/v1/policies/results.go
@@ -0,0 +1,246 @@
+package policies
+
+import (
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type policyResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any policyResult as a Policy, if possible.
+func (r policyResult) Extract() (*Policy, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var response struct {
+		Policy policy `mapstructure:"policy"`
+	}
+
+	if err := mapstructure.Decode(r.Body, &response); err != nil {
+		return nil, err
+	}
+
+	policy := response.Policy.toExported()
+
+	return &policy, nil
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	policyResult
+}
+
+// Extract extracts a slice of Policies from a CreateResult.  Multiple policies
+// can be created in a single operation, so the result of a create is always a
+// list of policies.
+func (res CreateResult) Extract() ([]Policy, error) {
+	if res.Err != nil {
+		return nil, res.Err
+	}
+
+	return commonExtractPolicies(res.Body)
+}
+
+// GetResult temporarily contains the response from a Get call.
+type GetResult struct {
+	policyResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// ExecuteResult represents the result of an execute operation.
+type ExecuteResult struct {
+	gophercloud.ErrResult
+}
+
+// Type represents a type of scaling policy.
+type Type string
+
+const (
+	// Schedule policies run at given times.
+	Schedule Type = "schedule"
+
+	// Webhook policies are triggered by HTTP requests.
+	Webhook Type = "webhook"
+)
+
+// AdjustmentType represents the way in which a policy will change a group.
+type AdjustmentType string
+
+// Valid types of adjustments for a policy.
+const (
+	Change          AdjustmentType = "change"
+	ChangePercent   AdjustmentType = "changePercent"
+	DesiredCapacity AdjustmentType = "desiredCapacity"
+)
+
+// ScheduleArgs is implemented by types that can be converted into arguments for
+// policies with type Schedule.
+type ScheduleArgs interface {
+	ToPolicyArgs() (map[string]string, error)
+}
+
+// At satisfies the ScheduleArgs interface and can be used to configure a policy
+// to execute a particular time.
+type At time.Time
+
+// ToPolicyArgs returns a key and value for use in constructing arguments to
+// schedule policies.
+func (at At) ToPolicyArgs() (map[string]string, error) {
+	t := time.Time(at)
+
+	args := make(map[string]string)
+	args["at"] = t.UTC().Format(time.RFC3339)
+
+	return args, nil
+}
+
+// Cron satisfies the ScheduleArgs interface and can be used to configure a
+// policy that executes at regular intervals.
+type Cron string
+
+// ToPolicyArgs returns a key and value for use in constructing arguments to
+// schedule policies.
+func (cron Cron) ToPolicyArgs() (map[string]string, error) {
+	if cron == "" {
+		return nil, ErrEmptyCron
+	}
+
+	args := make(map[string]string)
+	args["cron"] = string(cron)
+
+	return args, nil
+}
+
+// Policy represents a scaling policy.
+type Policy struct {
+	// UUID for the policy.
+	ID string
+
+	// Name of the policy.
+	Name string
+
+	// Type of scaling policy.
+	Type Type
+
+	// Cooldown period, in seconds.
+	Cooldown int
+
+	// The type of adjustment in capacity to be made.
+	AdjustmentType AdjustmentType
+
+	// The numeric value of the adjustment in capacity.
+	AdjustmentValue float64
+
+	// Arguments determining Schedule policy behavior, or nil for Webhook
+	// policies.
+	Schedule ScheduleArgs
+}
+
+// This is an intermediate representation of the exported Policy type.  The
+// fields in API responses vary by policy type and configuration.  This lets us
+// decode responses then normalize them into a Policy.
+type policy struct {
+	ID       string `mapstructure:"id"`
+	Name     string `mapstructure:"name"`
+	Type     Type   `mapstructure:"type"`
+	Cooldown int    `mapstructure:"cooldown"`
+
+	// The API will respond with exactly one of these omitting the others.
+	Change          interface{} `mapstructure:"change"`
+	ChangePercent   interface{} `mapstructure:"changePercent"`
+	DesiredCapacity interface{} `mapstructure:"desiredCapacity"`
+
+	// Additional configuration options for schedule policies.
+	Args map[string]string `mapstructure:"args"`
+}
+
+// Assemble a Policy from the intermediate policy struct.
+func (p policy) toExported() Policy {
+	policy := Policy{}
+
+	policy.ID = p.ID
+	policy.Name = p.Name
+	policy.Type = p.Type
+	policy.Cooldown = p.Cooldown
+
+	if cron, ok := p.Args["cron"]; ok {
+		policy.Schedule = Cron(cron)
+	} else if at, ok := p.Args["at"]; ok {
+		// Set an At schedule if the "at" argument parses as an RFC3339 time.
+		if t, err := time.Parse(time.RFC3339, at); err == nil {
+			policy.Schedule = At(t)
+		}
+	}
+
+	if v, ok := p.Change.(float64); ok {
+		policy.AdjustmentType = Change
+		policy.AdjustmentValue = v
+	} else if v, ok := p.ChangePercent.(float64); ok {
+		policy.AdjustmentType = ChangePercent
+		policy.AdjustmentValue = v
+	} else if v, ok := p.DesiredCapacity.(float64); ok {
+		policy.AdjustmentType = DesiredCapacity
+		policy.AdjustmentValue = v
+	}
+
+	return policy
+}
+
+// PolicyPage is the page returned by a pager when traversing over a collection
+// of scaling policies.
+type PolicyPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a page contains no Policy results.
+func (page PolicyPage) IsEmpty() (bool, error) {
+	policies, err := ExtractPolicies(page)
+
+	if err != nil {
+		return true, err
+	}
+
+	return len(policies) == 0, nil
+}
+
+// ExtractPolicies interprets the results of a single page from a List() call,
+// producing a slice of Policies.
+func ExtractPolicies(page pagination.Page) ([]Policy, error) {
+	return commonExtractPolicies(page.(PolicyPage).Body)
+}
+
+func commonExtractPolicies(body interface{}) ([]Policy, error) {
+	var response struct {
+		Policies []policy `mapstructure:"policies"`
+	}
+
+	err := mapstructure.Decode(body, &response)
+
+	if err != nil {
+		return nil, err
+	}
+
+	policies := make([]Policy, len(response.Policies))
+
+	for i, p := range response.Policies {
+		policies[i] = p.toExported()
+	}
+
+	return policies, nil
+}
diff --git a/rackspace/autoscale/v1/policies/urls.go b/rackspace/autoscale/v1/policies/urls.go
new file mode 100644
index 0000000..8ee3207
--- /dev/null
+++ b/rackspace/autoscale/v1/policies/urls.go
@@ -0,0 +1,27 @@
+package policies
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient, groupID string) string {
+	return c.ServiceURL("groups", groupID, "policies")
+}
+
+func createURL(c *gophercloud.ServiceClient, groupID string) string {
+	return c.ServiceURL("groups", groupID, "policies")
+}
+
+func getURL(c *gophercloud.ServiceClient, groupID, policyID string) string {
+	return c.ServiceURL("groups", groupID, "policies", policyID)
+}
+
+func updateURL(c *gophercloud.ServiceClient, groupID, policyID string) string {
+	return getURL(c, groupID, policyID)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, groupID, policyID string) string {
+	return getURL(c, groupID, policyID)
+}
+
+func executeURL(c *gophercloud.ServiceClient, groupID, policyID string) string {
+	return c.ServiceURL("groups", groupID, "policies", policyID, "execute")
+}
diff --git a/rackspace/autoscale/v1/policies/urls_test.go b/rackspace/autoscale/v1/policies/urls_test.go
new file mode 100644
index 0000000..774e2e7
--- /dev/null
+++ b/rackspace/autoscale/v1/policies/urls_test.go
@@ -0,0 +1,50 @@
+package policies
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+	actual := listURL(endpointClient(), "123")
+	expected := endpoint + "groups/123/policies"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := createURL(endpointClient(), "123")
+	expected := endpoint + "groups/123/policies"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := getURL(endpointClient(), "123", "456")
+	expected := endpoint + "groups/123/policies/456"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := updateURL(endpointClient(), "123", "456")
+	expected := endpoint + "groups/123/policies/456"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := deleteURL(endpointClient(), "123", "456")
+	expected := endpoint + "groups/123/policies/456"
+	th.CheckEquals(t, expected, actual)
+}
+
+func TestExecuteURL(t *testing.T) {
+	actual := executeURL(endpointClient(), "123", "456")
+	expected := endpoint + "groups/123/policies/456/execute"
+	th.CheckEquals(t, expected, actual)
+}