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)
+}