Merge pull request #357 from jrperritt/orchestration
OpenStack/Rackspace Orchestration
diff --git a/acceptance/openstack/orchestration/v1/buildinfo_test.go b/acceptance/openstack/orchestration/v1/buildinfo_test.go
new file mode 100644
index 0000000..05a5e1d
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/buildinfo_test.go
@@ -0,0 +1,20 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestBuildInfo(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ bi, err := buildinfo.Get(client).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("retrieved build info: %+v\n", bi)
+}
diff --git a/acceptance/openstack/orchestration/v1/common.go b/acceptance/openstack/orchestration/v1/common.go
new file mode 100644
index 0000000..2c28dcb
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/common.go
@@ -0,0 +1,44 @@
+// +build acceptance
+
+package v1
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var template = fmt.Sprintf(`
+{
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {},
+ "resources": {
+ "hello_world": {
+ "type":"OS::Nova::Server",
+ "properties": {
+ "flavor": "%s",
+ "image": "%s",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+}`, os.Getenv("OS_FLAVOR_ID"), os.Getenv("OS_IMAGE_ID"))
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+ ao, err := openstack.AuthOptionsFromEnv()
+ th.AssertNoErr(t, err)
+
+ client, err := openstack.AuthenticatedClient(ao)
+ th.AssertNoErr(t, err)
+
+ c, err := openstack.NewOrchestrationV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+ th.AssertNoErr(t, err)
+ return c
+}
diff --git a/acceptance/openstack/orchestration/v1/hello-compute.json b/acceptance/openstack/orchestration/v1/hello-compute.json
new file mode 100644
index 0000000..11cfc80
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/hello-compute.json
@@ -0,0 +1,13 @@
+{
+ "heat_template_version": "2013-05-23",
+ "resources": {
+ "compute_instance": {
+ "type": "OS::Nova::Server",
+ "properties": {
+ "flavor": "m1.small",
+ "image": "cirros-0.3.2-x86_64-disk",
+ "name": "Single Compute Instance"
+ }
+ }
+ }
+}
diff --git a/acceptance/openstack/orchestration/v1/stackevents_test.go b/acceptance/openstack/orchestration/v1/stackevents_test.go
new file mode 100644
index 0000000..e356c86
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stackevents_test.go
@@ -0,0 +1,68 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents"
+ "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackEvents(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ stackName := "postman_stack_2"
+ resourceName := "hello_world"
+ var eventID string
+
+ createOpts := stacks.CreateOpts{
+ Name: stackName,
+ Template: template,
+ Timeout: 5,
+ }
+ stack, err := stacks.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created stack: %+v\n", stack)
+ defer func() {
+ err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted stack (%s)", stackName)
+ }()
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "CREATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ err = stackevents.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) {
+ events, err := stackevents.ExtractEvents(page)
+ th.AssertNoErr(t, err)
+ t.Logf("listed events: %+v\n", events)
+ eventID = events[0].ID
+ return false, nil
+ })
+ th.AssertNoErr(t, err)
+
+ err = stackevents.ListResourceEvents(client, stackName, stack.ID, resourceName, nil).EachPage(func(page pagination.Page) (bool, error) {
+ resourceEvents, err := stackevents.ExtractEvents(page)
+ th.AssertNoErr(t, err)
+ t.Logf("listed resource events: %+v\n", resourceEvents)
+ return false, nil
+ })
+ th.AssertNoErr(t, err)
+
+ event, err := stackevents.Get(client, stackName, stack.ID, resourceName, eventID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("retrieved event: %+v\n", event)
+}
diff --git a/acceptance/openstack/orchestration/v1/stackresources_test.go b/acceptance/openstack/orchestration/v1/stackresources_test.go
new file mode 100644
index 0000000..b614f1c
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stackresources_test.go
@@ -0,0 +1,62 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+ "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackResources(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ stackName := "postman_stack_2"
+
+ createOpts := stacks.CreateOpts{
+ Name: stackName,
+ Template: template,
+ Timeout: 5,
+ }
+ stack, err := stacks.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created stack: %+v\n", stack)
+ defer func() {
+ err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted stack (%s)", stackName)
+ }()
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "CREATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ resourceName := "hello_world"
+ resource, err := stackresources.Get(client, stackName, stack.ID, resourceName).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Got stack resource: %+v\n", resource)
+
+ metadata, err := stackresources.Metadata(client, stackName, stack.ID, resourceName).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Got stack resource metadata: %+v\n", metadata)
+
+ err = stackresources.List(client, stackName, stack.ID, stackresources.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ resources, err := stackresources.ExtractResources(page)
+ th.AssertNoErr(t, err)
+ t.Logf("resources: %+v\n", resources)
+ return false, nil
+ })
+ th.AssertNoErr(t, err)
+}
diff --git a/acceptance/openstack/orchestration/v1/stacks_test.go b/acceptance/openstack/orchestration/v1/stacks_test.go
new file mode 100644
index 0000000..01e76d6
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stacks_test.go
@@ -0,0 +1,81 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStacks(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ stackName1 := "gophercloud-test-stack-2"
+ createOpts := stacks.CreateOpts{
+ Name: stackName1,
+ Template: template,
+ Timeout: 5,
+ }
+ stack, err := stacks.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created stack: %+v\n", stack)
+ defer func() {
+ err := stacks.Delete(client, stackName1, stack.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted stack (%s)", stackName1)
+ }()
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "CREATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ updateOpts := stacks.UpdateOpts{
+ Template: template,
+ Timeout: 20,
+ }
+ err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "UPDATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ t.Logf("Updated stack")
+
+ err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ stackList, err := stacks.ExtractStacks(page)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Got stack list: %+v\n", stackList)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Got stack: %+v\n", getStack)
+
+ abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Abandonded stack %+v\n", abandonedStack)
+ th.AssertNoErr(t, err)
+}
diff --git a/acceptance/openstack/orchestration/v1/stacktemplates_test.go b/acceptance/openstack/orchestration/v1/stacktemplates_test.go
new file mode 100644
index 0000000..14d8f44
--- /dev/null
+++ b/acceptance/openstack/orchestration/v1/stacktemplates_test.go
@@ -0,0 +1,77 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackTemplates(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ stackName := "postman_stack_2"
+
+ createOpts := stacks.CreateOpts{
+ Name: stackName,
+ Template: template,
+ Timeout: 5,
+ }
+ stack, err := stacks.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created stack: %+v\n", stack)
+ defer func() {
+ err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted stack (%s)", stackName)
+ }()
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "CREATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ tmpl, err := stacktemplates.Get(client, stackName, stack.ID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("retrieved template: %+v\n", tmpl)
+
+ validateOpts := stacktemplates.ValidateOpts{
+ Template: map[string]interface{}{
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": map[string]interface{}{
+ "flavor": map[string]interface{}{
+ "default": "m1.tiny",
+ "type": "string",
+ },
+ },
+ "resources": map[string]interface{}{
+ "hello_world": map[string]interface{}{
+ "type": "OS::Nova::Server",
+ "properties": map[string]interface{}{
+ "key_name": "heat_key",
+ "flavor": map[string]interface{}{
+ "get_param": "flavor",
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n",
+ },
+ },
+ },
+ },
+ }
+ validatedTemplate, err := stacktemplates.Validate(client, validateOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("validated template: %+v\n", validatedTemplate)
+}
diff --git a/acceptance/rackspace/orchestration/v1/buildinfo_test.go b/acceptance/rackspace/orchestration/v1/buildinfo_test.go
new file mode 100644
index 0000000..42cc048
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/buildinfo_test.go
@@ -0,0 +1,20 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/rackspace/orchestration/v1/buildinfo"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestBuildInfo(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ bi, err := buildinfo.Get(client).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("retrieved build info: %+v\n", bi)
+}
diff --git a/acceptance/rackspace/orchestration/v1/common.go b/acceptance/rackspace/orchestration/v1/common.go
new file mode 100644
index 0000000..b9d5197
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/common.go
@@ -0,0 +1,45 @@
+// +build acceptance
+
+package v1
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/rackspace"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+var template = fmt.Sprintf(`
+{
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {},
+ "resources": {
+ "hello_world": {
+ "type":"OS::Nova::Server",
+ "properties": {
+ "flavor": "%s",
+ "image": "%s",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+}
+`, os.Getenv("RS_FLAVOR_ID"), os.Getenv("RS_IMAGE_ID"))
+
+func newClient(t *testing.T) *gophercloud.ServiceClient {
+ ao, err := rackspace.AuthOptionsFromEnv()
+ th.AssertNoErr(t, err)
+
+ client, err := rackspace.AuthenticatedClient(ao)
+ th.AssertNoErr(t, err)
+
+ c, err := rackspace.NewOrchestrationV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("RS_REGION_NAME"),
+ })
+ th.AssertNoErr(t, err)
+ return c
+}
diff --git a/acceptance/rackspace/orchestration/v1/stackevents_test.go b/acceptance/rackspace/orchestration/v1/stackevents_test.go
new file mode 100644
index 0000000..9e3fc08
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/stackevents_test.go
@@ -0,0 +1,70 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ osStackEvents "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents"
+ osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackevents"
+ "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackEvents(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ stackName := "postman_stack_2"
+ resourceName := "hello_world"
+ var eventID string
+
+ createOpts := osStacks.CreateOpts{
+ Name: stackName,
+ Template: template,
+ Timeout: 5,
+ }
+ stack, err := stacks.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created stack: %+v\n", stack)
+ defer func() {
+ err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted stack (%s)", stackName)
+ }()
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "CREATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ err = stackevents.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) {
+ events, err := osStackEvents.ExtractEvents(page)
+ th.AssertNoErr(t, err)
+ t.Logf("listed events: %+v\n", events)
+ eventID = events[0].ID
+ return false, nil
+ })
+ th.AssertNoErr(t, err)
+
+ err = stackevents.ListResourceEvents(client, stackName, stack.ID, resourceName, nil).EachPage(func(page pagination.Page) (bool, error) {
+ resourceEvents, err := osStackEvents.ExtractResourceEvents(page)
+ th.AssertNoErr(t, err)
+ t.Logf("listed resource events: %+v\n", resourceEvents)
+ return false, nil
+ })
+ th.AssertNoErr(t, err)
+
+ event, err := stackevents.Get(client, stackName, stack.ID, resourceName, eventID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("retrieved event: %+v\n", event)
+}
diff --git a/acceptance/rackspace/orchestration/v1/stackresources_test.go b/acceptance/rackspace/orchestration/v1/stackresources_test.go
new file mode 100644
index 0000000..65926e7
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/stackresources_test.go
@@ -0,0 +1,64 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ osStackResources "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+ osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stackresources"
+ "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackResources(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ stackName := "postman_stack_2"
+
+ createOpts := osStacks.CreateOpts{
+ Name: stackName,
+ Template: template,
+ Timeout: 5,
+ }
+ stack, err := stacks.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created stack: %+v\n", stack)
+ defer func() {
+ err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted stack (%s)", stackName)
+ }()
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "CREATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ resourceName := "hello_world"
+ resource, err := stackresources.Get(client, stackName, stack.ID, resourceName).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Got stack resource: %+v\n", resource)
+
+ metadata, err := stackresources.Metadata(client, stackName, stack.ID, resourceName).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Got stack resource metadata: %+v\n", metadata)
+
+ err = stackresources.List(client, stackName, stack.ID, nil).EachPage(func(page pagination.Page) (bool, error) {
+ resources, err := osStackResources.ExtractResources(page)
+ th.AssertNoErr(t, err)
+ t.Logf("resources: %+v\n", resources)
+ return false, nil
+ })
+ th.AssertNoErr(t, err)
+}
diff --git a/acceptance/rackspace/orchestration/v1/stacks_test.go b/acceptance/rackspace/orchestration/v1/stacks_test.go
new file mode 100644
index 0000000..cfec4e9
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/stacks_test.go
@@ -0,0 +1,82 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStacks(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ stackName1 := "gophercloud-test-stack-2"
+ createOpts := osStacks.CreateOpts{
+ Name: stackName1,
+ Template: template,
+ Timeout: 5,
+ }
+ stack, err := stacks.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created stack: %+v\n", stack)
+ defer func() {
+ err := stacks.Delete(client, stackName1, stack.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted stack (%s)", stackName1)
+ }()
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "CREATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ updateOpts := osStacks.UpdateOpts{
+ Template: template,
+ Timeout: 20,
+ }
+ err = stacks.Update(client, stackName1, stack.ID, updateOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "UPDATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ t.Logf("Updated stack")
+
+ err = stacks.List(client, nil).EachPage(func(page pagination.Page) (bool, error) {
+ stackList, err := osStacks.ExtractStacks(page)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Got stack list: %+v\n", stackList)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ getStack, err := stacks.Get(client, stackName1, stack.ID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Got stack: %+v\n", getStack)
+
+ abandonedStack, err := stacks.Abandon(client, stackName1, stack.ID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Abandonded stack %+v\n", abandonedStack)
+ th.AssertNoErr(t, err)
+}
diff --git a/acceptance/rackspace/orchestration/v1/stacktemplates_test.go b/acceptance/rackspace/orchestration/v1/stacktemplates_test.go
new file mode 100644
index 0000000..1f7b217
--- /dev/null
+++ b/acceptance/rackspace/orchestration/v1/stacktemplates_test.go
@@ -0,0 +1,79 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ osStacks "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ osStacktemplates "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates"
+ "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/rackspace/orchestration/v1/stacktemplates"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestStackTemplates(t *testing.T) {
+ // Create a provider client for making the HTTP requests.
+ // See common.go in this directory for more information.
+ client := newClient(t)
+
+ stackName := "postman_stack_2"
+
+ createOpts := osStacks.CreateOpts{
+ Name: stackName,
+ Template: template,
+ Timeout: 5,
+ }
+ stack, err := stacks.Create(client, createOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created stack: %+v\n", stack)
+ defer func() {
+ err := stacks.Delete(client, stackName, stack.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+ t.Logf("Deleted stack (%s)", stackName)
+ }()
+ err = gophercloud.WaitFor(60, func() (bool, error) {
+ getStack, err := stacks.Get(client, stackName, stack.ID).Extract()
+ if err != nil {
+ return false, err
+ }
+ if getStack.Status == "CREATE_COMPLETE" {
+ return true, nil
+ }
+ return false, nil
+ })
+
+ tmpl, err := stacktemplates.Get(client, stackName, stack.ID).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("retrieved template: %+v\n", tmpl)
+
+ validateOpts := osStacktemplates.ValidateOpts{
+ Template: map[string]interface{}{
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": map[string]interface{}{
+ "flavor": map[string]interface{}{
+ "default": "m1.tiny",
+ "type": "string",
+ },
+ },
+ "resources": map[string]interface{}{
+ "hello_world": map[string]interface{}{
+ "type": "OS::Nova::Server",
+ "properties": map[string]interface{}{
+ "key_name": "heat_key",
+ "flavor": map[string]interface{}{
+ "get_param": "flavor",
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n",
+ },
+ },
+ },
+ },
+ }
+ validatedTemplate, err := stacktemplates.Validate(client, validateOpts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("validated template: %+v\n", validatedTemplate)
+}
diff --git a/openstack/client.go b/openstack/client.go
index 9c12dca..5fce3d6 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -214,3 +214,13 @@
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
+
+// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service.
+func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("orchestration")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/openstack/orchestration/v1/apiversions/doc.go b/openstack/orchestration/v1/apiversions/doc.go
new file mode 100644
index 0000000..f2db622
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/doc.go
@@ -0,0 +1,4 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Heat service. This functionality is not
+// restricted to this particular version.
+package apiversions
diff --git a/openstack/orchestration/v1/apiversions/requests.go b/openstack/orchestration/v1/apiversions/requests.go
new file mode 100644
index 0000000..f6454c8
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/requests.go
@@ -0,0 +1,13 @@
+package apiversions
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// ListVersions lists all the Neutron API versions available to end-users
+func ListVersions(c *gophercloud.ServiceClient) pagination.Pager {
+ return pagination.NewPager(c, apiVersionsURL(c), func(r pagination.PageResult) pagination.Page {
+ return APIVersionPage{pagination.SinglePageBase(r)}
+ })
+}
diff --git a/openstack/orchestration/v1/apiversions/requests_test.go b/openstack/orchestration/v1/apiversions/requests_test.go
new file mode 100644
index 0000000..a2fc980
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/requests_test.go
@@ -0,0 +1,89 @@
+package apiversions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "versions": [
+ {
+ "status": "CURRENT",
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8000/v1",
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+}`)
+ })
+
+ count := 0
+
+ ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAPIVersions(page)
+ if err != nil {
+ t.Errorf("Failed to extract API versions: %v", err)
+ return false, err
+ }
+
+ expected := []APIVersion{
+ APIVersion{
+ Status: "CURRENT",
+ ID: "v1.0",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://23.253.228.211:8000/v1",
+ Rel: "self",
+ },
+ },
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestNonJSONCannotBeExtractedIntoAPIVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ if _, err := ExtractAPIVersions(page); err == nil {
+ t.Fatalf("Expected error, got nil")
+ }
+ return true, nil
+ })
+}
diff --git a/openstack/orchestration/v1/apiversions/results.go b/openstack/orchestration/v1/apiversions/results.go
new file mode 100644
index 0000000..0700ab0
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/results.go
@@ -0,0 +1,42 @@
+package apiversions
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// APIVersion represents an API version for Neutron. It contains the status of
+// the API, and its unique ID.
+type APIVersion struct {
+ Status string `mapstructure:"status"`
+ ID string `mapstructure:"id"`
+ Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// APIVersionPage is the page returned by a pager when traversing over a
+// collection of API versions.
+type APIVersionPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an APIVersionPage struct is empty.
+func (r APIVersionPage) IsEmpty() (bool, error) {
+ is, err := ExtractAPIVersions(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractAPIVersions takes a collection page, extracts all of the elements,
+// and returns them a slice of APIVersion structs. It is effectively a cast.
+func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) {
+ var resp struct {
+ Versions []APIVersion `mapstructure:"versions"`
+ }
+
+ err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
+
+ return resp.Versions, err
+}
diff --git a/openstack/orchestration/v1/apiversions/urls.go b/openstack/orchestration/v1/apiversions/urls.go
new file mode 100644
index 0000000..55d6e0e
--- /dev/null
+++ b/openstack/orchestration/v1/apiversions/urls.go
@@ -0,0 +1,7 @@
+package apiversions
+
+import "github.com/rackspace/gophercloud"
+
+func apiVersionsURL(c *gophercloud.ServiceClient) string {
+ return c.Endpoint
+}
diff --git a/openstack/orchestration/v1/buildinfo/doc.go b/openstack/orchestration/v1/buildinfo/doc.go
new file mode 100644
index 0000000..183e8df
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/doc.go
@@ -0,0 +1,2 @@
+// Package buildinfo provides build information about heat deployments.
+package buildinfo
diff --git a/openstack/orchestration/v1/buildinfo/fixtures.go b/openstack/orchestration/v1/buildinfo/fixtures.go
new file mode 100644
index 0000000..20ea09b
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/fixtures.go
@@ -0,0 +1,45 @@
+package buildinfo
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &BuildInfo{
+ API: Revision{
+ Revision: "2.4.5",
+ },
+ Engine: Revision{
+ Revision: "1.2.1",
+ },
+}
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+ "api": {
+ "revision": "2.4.5"
+ },
+ "engine": {
+ "revision": "1.2.1"
+ }
+}`
+
+// HandleGetSuccessfully creates an HTTP handler at `/build_info`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/build_info", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
diff --git a/openstack/orchestration/v1/buildinfo/requests.go b/openstack/orchestration/v1/buildinfo/requests.go
new file mode 100644
index 0000000..c20b48c
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/requests.go
@@ -0,0 +1,17 @@
+package buildinfo
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// Get retreives data for the given stack template.
+func Get(c *gophercloud.ServiceClient) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
diff --git a/openstack/orchestration/v1/buildinfo/requests_test.go b/openstack/orchestration/v1/buildinfo/requests_test.go
new file mode 100644
index 0000000..1e0fe23
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/requests_test.go
@@ -0,0 +1,20 @@
+package buildinfo
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t, GetOutput)
+
+ actual, err := Get(fake.ServiceClient()).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := GetExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/buildinfo/results.go b/openstack/orchestration/v1/buildinfo/results.go
new file mode 100644
index 0000000..683a434
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/results.go
@@ -0,0 +1,37 @@
+package buildinfo
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+)
+
+// Revision represents the API/Engine revision of a Heat deployment.
+type Revision struct {
+ Revision string `mapstructure:"revision"`
+}
+
+// BuildInfo represents the build information for a Heat deployment.
+type BuildInfo struct {
+ API Revision `mapstructure:"api"`
+ Engine Revision `mapstructure:"engine"`
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to a BuildInfo object and is called after a
+// Get operation.
+func (r GetResult) Extract() (*BuildInfo, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res BuildInfo
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ return &res, nil
+}
diff --git a/openstack/orchestration/v1/buildinfo/urls.go b/openstack/orchestration/v1/buildinfo/urls.go
new file mode 100644
index 0000000..2c873d0
--- /dev/null
+++ b/openstack/orchestration/v1/buildinfo/urls.go
@@ -0,0 +1,7 @@
+package buildinfo
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("build_info")
+}
diff --git a/openstack/orchestration/v1/stackevents/doc.go b/openstack/orchestration/v1/stackevents/doc.go
new file mode 100644
index 0000000..51cdd97
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/doc.go
@@ -0,0 +1,4 @@
+// Package stackevents provides operations for finding, listing, and retrieving
+// stack events. Stack events are events that take place on stacks such as
+// updating and abandoning.
+package stackevents
diff --git a/openstack/orchestration/v1/stackevents/fixtures.go b/openstack/orchestration/v1/stackevents/fixtures.go
new file mode 100644
index 0000000..016ae00
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/fixtures.go
@@ -0,0 +1,446 @@
+package stackevents
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// FindExpected represents the expected object from a Find request.
+var FindExpected = []Event{
+ Event{
+ ResourceName: "hello_world",
+ Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ Rel: "resource",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ Rel: "stack",
+ },
+ },
+ LogicalResourceID: "hello_world",
+ ResourceStatusReason: "state changed",
+ ResourceStatus: "CREATE_IN_PROGRESS",
+ PhysicalResourceID: "",
+ ID: "06feb26f-9298-4a9b-8749-9d770e5d577a",
+ },
+ Event{
+ ResourceName: "hello_world",
+ Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ Rel: "resource",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ Rel: "stack",
+ },
+ },
+ LogicalResourceID: "hello_world",
+ ResourceStatusReason: "state changed",
+ ResourceStatus: "CREATE_COMPLETE",
+ PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf",
+ ID: "93940999-7d40-44ae-8de4-19624e7b8d18",
+ },
+}
+
+// FindOutput represents the response body from a Find request.
+const FindOutput = `
+{
+ "events": [
+ {
+ "resource_name": "hello_world",
+ "event_time": "2015-02-05T21:33:11Z",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ "rel": "resource"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "hello_world",
+ "resource_status_reason": "state changed",
+ "resource_status": "CREATE_IN_PROGRESS",
+ "physical_resource_id": null,
+ "id": "06feb26f-9298-4a9b-8749-9d770e5d577a"
+ },
+ {
+ "resource_name": "hello_world",
+ "event_time": "2015-02-05T21:33:27Z",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ "rel": "resource"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "hello_world",
+ "resource_status_reason": "state changed",
+ "resource_status": "CREATE_COMPLETE",
+ "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+ "id": "93940999-7d40-44ae-8de4-19624e7b8d18"
+ }
+ ]
+}`
+
+// HandleFindSuccessfully creates an HTTP handler at `/stacks/postman_stack/events`
+// on the test handler mux that responds with a `Find` response.
+func HandleFindSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/postman_stack/events", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// ListExpected represents the expected object from a List request.
+var ListExpected = []Event{
+ Event{
+ ResourceName: "hello_world",
+ Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ Rel: "resource",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ Rel: "stack",
+ },
+ },
+ LogicalResourceID: "hello_world",
+ ResourceStatusReason: "state changed",
+ ResourceStatus: "CREATE_IN_PROGRESS",
+ PhysicalResourceID: "",
+ ID: "06feb26f-9298-4a9b-8749-9d770e5d577a",
+ },
+ Event{
+ ResourceName: "hello_world",
+ Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ Rel: "resource",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ Rel: "stack",
+ },
+ },
+ LogicalResourceID: "hello_world",
+ ResourceStatusReason: "state changed",
+ ResourceStatus: "CREATE_COMPLETE",
+ PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf",
+ ID: "93940999-7d40-44ae-8de4-19624e7b8d18",
+ },
+}
+
+// ListOutput represents the response body from a List request.
+const ListOutput = `
+{
+ "events": [
+ {
+ "resource_name": "hello_world",
+ "event_time": "2015-02-05T21:33:11Z",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ "rel": "resource"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "hello_world",
+ "resource_status_reason": "state changed",
+ "resource_status": "CREATE_IN_PROGRESS",
+ "physical_resource_id": null,
+ "id": "06feb26f-9298-4a9b-8749-9d770e5d577a"
+ },
+ {
+ "resource_name": "hello_world",
+ "event_time": "2015-02-05T21:33:27Z",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ "rel": "resource"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "hello_world",
+ "resource_status_reason": "state changed",
+ "resource_status": "CREATE_COMPLETE",
+ "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+ "id": "93940999-7d40-44ae-8de4-19624e7b8d18"
+ }
+ ]
+}`
+
+// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events`
+// on the test handler mux that responds with a `List` response.
+func HandleListSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/events", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, output)
+ case "93940999-7d40-44ae-8de4-19624e7b8d18":
+ fmt.Fprintf(w, `{"events":[]}`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+}
+
+// ListResourceEventsExpected represents the expected object from a ListResourceEvents request.
+var ListResourceEventsExpected = []Event{
+ Event{
+ ResourceName: "hello_world",
+ Time: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ Rel: "resource",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ Rel: "stack",
+ },
+ },
+ LogicalResourceID: "hello_world",
+ ResourceStatusReason: "state changed",
+ ResourceStatus: "CREATE_IN_PROGRESS",
+ PhysicalResourceID: "",
+ ID: "06feb26f-9298-4a9b-8749-9d770e5d577a",
+ },
+ Event{
+ ResourceName: "hello_world",
+ Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ Rel: "resource",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ Rel: "stack",
+ },
+ },
+ LogicalResourceID: "hello_world",
+ ResourceStatusReason: "state changed",
+ ResourceStatus: "CREATE_COMPLETE",
+ PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf",
+ ID: "93940999-7d40-44ae-8de4-19624e7b8d18",
+ },
+}
+
+// ListResourceEventsOutput represents the response body from a ListResourceEvents request.
+const ListResourceEventsOutput = `
+{
+ "events": [
+ {
+ "resource_name": "hello_world",
+ "event_time": "2015-02-05T21:33:11Z",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/06feb26f-9298-4a9b-8749-9d770e5d577a",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ "rel": "resource"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "hello_world",
+ "resource_status_reason": "state changed",
+ "resource_status": "CREATE_IN_PROGRESS",
+ "physical_resource_id": null,
+ "id": "06feb26f-9298-4a9b-8749-9d770e5d577a"
+ },
+ {
+ "resource_name": "hello_world",
+ "event_time": "2015-02-05T21:33:27Z",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ "rel": "resource"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "hello_world",
+ "resource_status_reason": "state changed",
+ "resource_status": "CREATE_COMPLETE",
+ "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+ "id": "93940999-7d40-44ae-8de4-19624e7b8d18"
+ }
+ ]
+}`
+
+// HandleListResourceEventsSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events`
+// on the test handler mux that responds with a `ListResourceEvents` response.
+func HandleListResourceEventsSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, output)
+ case "93940999-7d40-44ae-8de4-19624e7b8d18":
+ fmt.Fprintf(w, `{"events":[]}`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+}
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &Event{
+ ResourceName: "hello_world",
+ Time: time.Date(2015, 2, 5, 21, 33, 27, 0, time.UTC),
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ Rel: "resource",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ Rel: "stack",
+ },
+ },
+ LogicalResourceID: "hello_world",
+ ResourceStatusReason: "state changed",
+ ResourceStatus: "CREATE_COMPLETE",
+ PhysicalResourceID: "49181cd6-169a-4130-9455-31185bbfc5bf",
+ ID: "93940999-7d40-44ae-8de4-19624e7b8d18",
+}
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+ "event":{
+ "resource_name": "hello_world",
+ "event_time": "2015-02-05T21:33:27Z",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world/events/93940999-7d40-44ae-8de4-19624e7b8d18",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ "rel": "resource"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "hello_world",
+ "resource_status_reason": "state changed",
+ "resource_status": "CREATE_COMPLETE",
+ "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+ "id": "93940999-7d40-44ae-8de4-19624e7b8d18"
+ }
+}`
+
+// HandleGetSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events/93940999-7d40-44ae-8de4-19624e7b8d18`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources/my_resource/events/93940999-7d40-44ae-8de4-19624e7b8d18", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
diff --git a/openstack/orchestration/v1/stackevents/requests.go b/openstack/orchestration/v1/stackevents/requests.go
new file mode 100644
index 0000000..80808b1
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/requests.go
@@ -0,0 +1,208 @@
+package stackevents
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Find retrieves stack events for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) FindResult {
+ var res FindResult
+
+ _, res.Err = perigee.Request("GET", findURL(c, stackName), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// SortDir is a type for specifying in which direction to sort a list of events.
+type SortDir string
+
+// SortKey is a type for specifying by which key to sort a list of events.
+type SortKey string
+
+// ResourceStatus is a type for specifying by which resource status to filter a
+// list of events.
+type ResourceStatus string
+
+// ResourceAction is a type for specifying by which resource action to filter a
+// list of events.
+type ResourceAction string
+
+var (
+ // ResourceStatusInProgress is used to filter a List request by the 'IN_PROGRESS' status.
+ ResourceStatusInProgress ResourceStatus = "IN_PROGRESS"
+ // ResourceStatusComplete is used to filter a List request by the 'COMPLETE' status.
+ ResourceStatusComplete ResourceStatus = "COMPLETE"
+ // ResourceStatusFailed is used to filter a List request by the 'FAILED' status.
+ ResourceStatusFailed ResourceStatus = "FAILED"
+
+ // ResourceActionCreate is used to filter a List request by the 'CREATE' action.
+ ResourceActionCreate ResourceAction = "CREATE"
+ // ResourceActionDelete is used to filter a List request by the 'DELETE' action.
+ ResourceActionDelete ResourceAction = "DELETE"
+ // ResourceActionUpdate is used to filter a List request by the 'UPDATE' action.
+ ResourceActionUpdate ResourceAction = "UPDATE"
+ // ResourceActionRollback is used to filter a List request by the 'ROLLBACK' action.
+ ResourceActionRollback ResourceAction = "ROLLBACK"
+ // ResourceActionSuspend is used to filter a List request by the 'SUSPEND' action.
+ ResourceActionSuspend ResourceAction = "SUSPEND"
+ // ResourceActionResume is used to filter a List request by the 'RESUME' action.
+ ResourceActionResume ResourceAction = "RESUME"
+ // ResourceActionAbandon is used to filter a List request by the 'ABANDON' action.
+ ResourceActionAbandon ResourceAction = "ABANDON"
+
+ // SortAsc is used to sort a list of stacks in ascending order.
+ SortAsc SortDir = "asc"
+ // SortDesc is used to sort a list of stacks in descending order.
+ SortDesc SortDir = "desc"
+
+ // SortName is used to sort a list of stacks by name.
+ SortName SortKey = "name"
+ // SortResourceType is used to sort a list of stacks by resource type.
+ SortResourceType SortKey = "resource_type"
+ // SortCreatedAt is used to sort a list of stacks by date created.
+ SortCreatedAt SortKey = "created_at"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToStackEventListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Marker and Limit are used for pagination.
+type ListOpts struct {
+ // The stack resource ID with which to start the listing.
+ Marker string `q:"marker"`
+ // Integer value for the limit of values to return.
+ Limit int `q:"limit"`
+ // Filters the event list by the specified ResourceAction. You can use this
+ // filter multiple times to filter by multiple resource actions: CREATE, DELETE,
+ // UPDATE, ROLLBACK, SUSPEND, RESUME or ADOPT.
+ ResourceActions []ResourceAction `q:"resource_action"`
+ // Filters the event list by the specified resource_status. You can use this
+ // filter multiple times to filter by multiple resource statuses: IN_PROGRESS,
+ // COMPLETE or FAILED.
+ ResourceStatuses []ResourceStatus `q:"resource_status"`
+ // Filters the event list by the specified resource_name. You can use this
+ // filter multiple times to filter by multiple resource names.
+ ResourceNames []string `q:"resource_name"`
+ // Filters the event list by the specified resource_type. You can use this
+ // filter multiple times to filter by multiple resource types: OS::Nova::Server,
+ // OS::Cinder::Volume, and so on.
+ ResourceTypes []string `q:"resource_type"`
+ // Sorts the event list by: resource_type or created_at.
+ SortKey SortKey `q:"sort_keys"`
+ // The sort direction of the event list. Which is asc (ascending) or desc (descending).
+ SortDir SortDir `q:"sort_dir"`
+}
+
+// ToStackEventListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToStackEventListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List makes a request against the API to list resources for the given stack.
+func List(client *gophercloud.ServiceClient, stackName, stackID string, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(client, stackName, stackID)
+
+ if opts != nil {
+ query, err := opts.ToStackEventListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ p := EventPage{pagination.MarkerPageBase{PageResult: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ return pagination.NewPager(client, url, createPageFn)
+}
+
+// ListResourceEventsOptsBuilder allows extensions to add additional parameters to the
+// ListResourceEvents request.
+type ListResourceEventsOptsBuilder interface {
+ ToResourceEventListQuery() (string, error)
+}
+
+// ListResourceEventsOpts allows the filtering and sorting of paginated resource events through
+// the API. Marker and Limit are used for pagination.
+type ListResourceEventsOpts struct {
+ // The stack resource ID with which to start the listing.
+ Marker string `q:"marker"`
+ // Integer value for the limit of values to return.
+ Limit int `q:"limit"`
+ // Filters the event list by the specified ResourceAction. You can use this
+ // filter multiple times to filter by multiple resource actions: CREATE, DELETE,
+ // UPDATE, ROLLBACK, SUSPEND, RESUME or ADOPT.
+ ResourceActions []string `q:"resource_action"`
+ // Filters the event list by the specified resource_status. You can use this
+ // filter multiple times to filter by multiple resource statuses: IN_PROGRESS,
+ // COMPLETE or FAILED.
+ ResourceStatuses []string `q:"resource_status"`
+ // Filters the event list by the specified resource_name. You can use this
+ // filter multiple times to filter by multiple resource names.
+ ResourceNames []string `q:"resource_name"`
+ // Filters the event list by the specified resource_type. You can use this
+ // filter multiple times to filter by multiple resource types: OS::Nova::Server,
+ // OS::Cinder::Volume, and so on.
+ ResourceTypes []string `q:"resource_type"`
+ // Sorts the event list by: resource_type or created_at.
+ SortKey SortKey `q:"sort_keys"`
+ // The sort direction of the event list. Which is asc (ascending) or desc (descending).
+ SortDir SortDir `q:"sort_dir"`
+}
+
+// ToResourceEventsListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToResourceEventsListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// ListResourceEvents makes a request against the API to list resources for the given stack.
+func ListResourceEvents(client *gophercloud.ServiceClient, stackName, stackID, resourceName string, opts ListResourceEventsOptsBuilder) pagination.Pager {
+ url := listResourceEventsURL(client, stackName, stackID, resourceName)
+
+ if opts != nil {
+ query, err := opts.ToResourceEventListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ p := EventPage{pagination.MarkerPageBase{PageResult: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ return pagination.NewPager(client, url, createPageFn)
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c, stackName, stackID, resourceName, eventID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
diff --git a/openstack/orchestration/v1/stackevents/requests_test.go b/openstack/orchestration/v1/stackevents/requests_test.go
new file mode 100644
index 0000000..a4da4d0
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/requests_test.go
@@ -0,0 +1,71 @@
+package stackevents
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindEvents(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleFindSuccessfully(t, FindOutput)
+
+ actual, err := Find(fake.ServiceClient(), "postman_stack").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := FindExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListSuccessfully(t, ListOutput)
+
+ count := 0
+ err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractEvents(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ListExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
+
+func TestListResourceEvents(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListResourceEventsSuccessfully(t, ListResourceEventsOutput)
+
+ count := 0
+ err := ListResourceEvents(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractEvents(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ListResourceEventsExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
+
+func TestGetEvent(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t, GetOutput)
+
+ actual, err := Get(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", "93940999-7d40-44ae-8de4-19624e7b8d18").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := GetExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/stackevents/results.go b/openstack/orchestration/v1/stackevents/results.go
new file mode 100644
index 0000000..bf233ae
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/results.go
@@ -0,0 +1,162 @@
+package stackevents
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Event represents a stack event.
+type Event struct {
+ // The name of the resource for which the event occurred.
+ ResourceName string `mapstructure:"resource_name"`
+ // The time the event occurred.
+ Time time.Time `mapstructure:"-"`
+ // The URLs to the event.
+ Links []gophercloud.Link `mapstructure:"links"`
+ // The logical ID of the stack resource.
+ LogicalResourceID string `mapstructure:"logical_resource_id"`
+ // The reason of the status of the event.
+ ResourceStatusReason string `mapstructure:"resource_status_reason"`
+ // The status of the event.
+ ResourceStatus string `mapstructure:"resource_status"`
+ // The physical ID of the stack resource.
+ PhysicalResourceID string `mapstructure:"physical_resource_id"`
+ // The event ID.
+ ID string `mapstructure:"id"`
+ // Properties of the stack resource.
+ ResourceProperties map[string]interface{} `mapstructure:"resource_properties"`
+}
+
+// FindResult represents the result of a Find operation.
+type FindResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a slice of Event objects and is called after a
+// Find operation.
+func (r FindResult) Extract() ([]Event, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Res []Event `mapstructure:"events"`
+ }
+
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ events := r.Body.(map[string]interface{})["events"].([]interface{})
+
+ for i, eventRaw := range events {
+ event := eventRaw.(map[string]interface{})
+ if date, ok := event["event_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ res.Res[i].Time = t
+ }
+ }
+
+ return res.Res, nil
+}
+
+// EventPage abstracts the raw results of making a List() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
+// data provided through the ExtractResources call.
+type EventPage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a page contains no Server results.
+func (r EventPage) IsEmpty() (bool, error) {
+ events, err := ExtractEvents(r)
+ if err != nil {
+ return true, err
+ }
+ return len(events) == 0, nil
+}
+
+// LastMarker returns the last stack ID in a ListResult.
+func (r EventPage) LastMarker() (string, error) {
+ events, err := ExtractEvents(r)
+ if err != nil {
+ return "", err
+ }
+ if len(events) == 0 {
+ return "", nil
+ }
+ return events[len(events)-1].ID, nil
+}
+
+// ExtractEvents interprets the results of a single page from a List() call, producing a slice of Event entities.
+func ExtractEvents(page pagination.Page) ([]Event, error) {
+ casted := page.(EventPage).Body
+
+ var res struct {
+ Res []Event `mapstructure:"events"`
+ }
+
+ if err := mapstructure.Decode(casted, &res); err != nil {
+ return nil, err
+ }
+
+ events := casted.(map[string]interface{})["events"].([]interface{})
+
+ for i, eventRaw := range events {
+ event := eventRaw.(map[string]interface{})
+ if date, ok := event["event_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ res.Res[i].Time = t
+ }
+ }
+
+ return res.Res, nil
+}
+
+// ExtractResourceEvents interprets the results of a single page from a
+// ListResourceEvents() call, producing a slice of Event entities.
+func ExtractResourceEvents(page pagination.Page) ([]Event, error) {
+ return ExtractEvents(page)
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to an Event object and is called after a
+// Get operation.
+func (r GetResult) Extract() (*Event, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Res *Event `mapstructure:"event"`
+ }
+
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ event := r.Body.(map[string]interface{})["event"].(map[string]interface{})
+
+ if date, ok := event["event_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ res.Res.Time = t
+ }
+
+ return res.Res, nil
+}
diff --git a/openstack/orchestration/v1/stackevents/urls.go b/openstack/orchestration/v1/stackevents/urls.go
new file mode 100644
index 0000000..8b5eceb
--- /dev/null
+++ b/openstack/orchestration/v1/stackevents/urls.go
@@ -0,0 +1,19 @@
+package stackevents
+
+import "github.com/rackspace/gophercloud"
+
+func findURL(c *gophercloud.ServiceClient, stackName string) string {
+ return c.ServiceURL("stacks", stackName, "events")
+}
+
+func listURL(c *gophercloud.ServiceClient, stackName, stackID string) string {
+ return c.ServiceURL("stacks", stackName, stackID, "events")
+}
+
+func listResourceEventsURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string {
+ return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "events")
+}
+
+func getURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) string {
+ return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "events", eventID)
+}
diff --git a/openstack/orchestration/v1/stackresources/doc.go b/openstack/orchestration/v1/stackresources/doc.go
new file mode 100644
index 0000000..e4f8b08
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/doc.go
@@ -0,0 +1,5 @@
+// Package stackresources provides operations for working with stack resources.
+// A resource is a template artifact that represents some component of your
+// desired architecture (a Cloud Server, a group of scaled Cloud Servers, a load
+// balancer, some configuration management system, and so forth).
+package stackresources
diff --git a/openstack/orchestration/v1/stackresources/fixtures.go b/openstack/orchestration/v1/stackresources/fixtures.go
new file mode 100644
index 0000000..0b930f4
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/fixtures.go
@@ -0,0 +1,451 @@
+package stackresources
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// FindExpected represents the expected object from a Find request.
+var FindExpected = []Resource{
+ Resource{
+ Name: "hello_world",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ Rel: "stack",
+ },
+ },
+ LogicalID: "hello_world",
+ StatusReason: "state changed",
+ UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+ RequiredBy: []interface{}{},
+ Status: "CREATE_IN_PROGRESS",
+ PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf",
+ Type: "OS::Nova::Server",
+ },
+}
+
+// FindOutput represents the response body from a Find request.
+const FindOutput = `
+{
+ "resources": [
+ {
+ "resource_name": "hello_world",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "hello_world",
+ "resource_status_reason": "state changed",
+ "updated_time": "2015-02-05T21:33:11Z",
+ "required_by": [],
+ "resource_status": "CREATE_IN_PROGRESS",
+ "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+ "resource_type": "OS::Nova::Server"
+ }
+ ]
+}`
+
+// HandleFindSuccessfully creates an HTTP handler at `/stacks/hello_world/resources`
+// on the test handler mux that responds with a `Find` response.
+func HandleFindSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/hello_world/resources", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// ListExpected represents the expected object from a List request.
+var ListExpected = []Resource{
+ Resource{
+ Name: "hello_world",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ Rel: "stack",
+ },
+ },
+ LogicalID: "hello_world",
+ StatusReason: "state changed",
+ UpdatedTime: time.Date(2015, 2, 5, 21, 33, 11, 0, time.UTC),
+ RequiredBy: []interface{}{},
+ Status: "CREATE_IN_PROGRESS",
+ PhysicalID: "49181cd6-169a-4130-9455-31185bbfc5bf",
+ Type: "OS::Nova::Server",
+ },
+}
+
+// ListOutput represents the response body from a List request.
+const ListOutput = `{
+ "resources": [
+ {
+ "resource_name": "hello_world",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b/resources/hello_world",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/5f57cff9-93fc-424e-9f78-df0515e7f48b",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "hello_world",
+ "resource_status_reason": "state changed",
+ "updated_time": "2015-02-05T21:33:11Z",
+ "required_by": [],
+ "resource_status": "CREATE_IN_PROGRESS",
+ "physical_resource_id": "49181cd6-169a-4130-9455-31185bbfc5bf",
+ "resource_type": "OS::Nova::Server"
+ }
+]
+}`
+
+// HandleListSuccessfully creates an HTTP handler at `/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources`
+// on the test handler mux that responds with a `List` response.
+func HandleListSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/hello_world/49181cd6-169a-4130-9455-31185bbfc5bf/resources", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, output)
+ case "49181cd6-169a-4130-9455-31185bbfc5bf":
+ fmt.Fprintf(w, `{"resources":[]}`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+}
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &Resource{
+ Name: "wordpress_instance",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance",
+ Rel: "self",
+ },
+ gophercloud.Link{
+ Href: "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e",
+ Rel: "stack",
+ },
+ },
+ LogicalID: "wordpress_instance",
+ StatusReason: "state changed",
+ UpdatedTime: time.Date(2014, 12, 10, 18, 34, 35, 0, time.UTC),
+ RequiredBy: []interface{}{},
+ Status: "CREATE_COMPLETE",
+ PhysicalID: "00e3a2fe-c65d-403c-9483-4db9930dd194",
+ Type: "OS::Nova::Server",
+}
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+ "resource": {
+ "resource_name": "wordpress_instance",
+ "description": "",
+ "links": [
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance",
+ "rel": "self"
+ },
+ {
+ "href": "http://166.78.160.107:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e",
+ "rel": "stack"
+ }
+ ],
+ "logical_resource_id": "wordpress_instance",
+ "resource_status": "CREATE_COMPLETE",
+ "updated_time": "2014-12-10T18:34:35Z",
+ "required_by": [],
+ "resource_status_reason": "state changed",
+ "physical_resource_id": "00e3a2fe-c65d-403c-9483-4db9930dd194",
+ "resource_type": "OS::Nova::Server"
+ }
+}`
+
+// HandleGetSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// MetadataExpected represents the expected object from a Metadata request.
+var MetadataExpected = map[string]string{
+ "number": "7",
+ "animal": "auk",
+}
+
+// MetadataOutput represents the response body from a Metadata request.
+const MetadataOutput = `
+{
+ "metadata": {
+ "number": "7",
+ "animal": "auk"
+ }
+}`
+
+// HandleMetadataSuccessfully creates an HTTP handler at `/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata`
+// on the test handler mux that responds with a `Metadata` response.
+func HandleMetadataSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/teststack/0b1771bd-9336-4f2b-ae86-a80f971faf1e/resources/wordpress_instance/metadata", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// ListTypesExpected represents the expected object from a ListTypes request.
+var ListTypesExpected = []string{
+ "OS::Nova::Server",
+ "OS::Heat::RandomString",
+ "OS::Swift::Container",
+ "OS::Trove::Instance",
+ "OS::Nova::FloatingIPAssociation",
+ "OS::Cinder::VolumeAttachment",
+ "OS::Nova::FloatingIP",
+ "OS::Nova::KeyPair",
+}
+
+// ListTypesOutput represents the response body from a ListTypes request.
+const ListTypesOutput = `
+{
+ "resource_types": [
+ "OS::Nova::Server",
+ "OS::Heat::RandomString",
+ "OS::Swift::Container",
+ "OS::Trove::Instance",
+ "OS::Nova::FloatingIPAssociation",
+ "OS::Cinder::VolumeAttachment",
+ "OS::Nova::FloatingIP",
+ "OS::Nova::KeyPair"
+ ]
+}`
+
+// HandleListTypesSuccessfully creates an HTTP handler at `/resource_types`
+// on the test handler mux that responds with a `ListTypes` response.
+func HandleListTypesSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/resource_types", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// GetSchemaExpected represents the expected object from a Schema request.
+var GetSchemaExpected = &TypeSchema{
+ Attributes: map[string]interface{}{
+ "an_attribute": map[string]interface{}{
+ "description": "An attribute description .",
+ },
+ },
+ Properties: map[string]interface{}{
+ "a_property": map[string]interface{}{
+ "update_allowed": false,
+ "required": true,
+ "type": "string",
+ "description": "A resource description.",
+ },
+ },
+ ResourceType: "OS::Heat::AResourceName",
+}
+
+// GetSchemaOutput represents the response body from a Schema request.
+const GetSchemaOutput = `
+{
+ "attributes": {
+ "an_attribute": {
+ "description": "An attribute description ."
+ }
+ },
+ "properties": {
+ "a_property": {
+ "update_allowed": false,
+ "required": true,
+ "type": "string",
+ "description": "A resource description."
+ }
+ },
+ "resource_type": "OS::Heat::AResourceName"
+}`
+
+// HandleGetSchemaSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName`
+// on the test handler mux that responds with a `Schema` response.
+func HandleGetSchemaSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/resource_types/OS::Heat::AResourceName", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// GetTemplateExpected represents the expected object from a Template request.
+var GetTemplateExpected = &TypeTemplate{
+ HeatTemplateFormatVersion: "2012-12-12",
+ Outputs: map[string]interface{}{
+ "private_key": map[string]interface{}{
+ "Description": "The private key if it has been saved.",
+ "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"private_key\"]}",
+ },
+ "public_key": map[string]interface{}{
+ "Description": "The public key.",
+ "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"public_key\"]}",
+ },
+ },
+ Parameters: map[string]interface{}{
+ "name": map[string]interface{}{
+ "Description": "The name of the key pair.",
+ "Type": "String",
+ },
+ "public_key": map[string]interface{}{
+ "Description": "The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.",
+ "Type": "String",
+ },
+ "save_private_key": map[string]interface{}{
+ "AllowedValues": []string{
+ "True",
+ "true",
+ "False",
+ "false",
+ },
+ "Default": false,
+ "Description": "True if the system should remember a generated private key; False otherwise.",
+ "Type": "String",
+ },
+ },
+ Resources: map[string]interface{}{
+ "KeyPair": map[string]interface{}{
+ "Properties": map[string]interface{}{
+ "name": map[string]interface{}{
+ "Ref": "name",
+ },
+ "public_key": map[string]interface{}{
+ "Ref": "public_key",
+ },
+ "save_private_key": map[string]interface{}{
+ "Ref": "save_private_key",
+ },
+ },
+ "Type": "OS::Nova::KeyPair",
+ },
+ },
+}
+
+// GetTemplateOutput represents the response body from a Template request.
+const GetTemplateOutput = `
+{
+ "HeatTemplateFormatVersion": "2012-12-12",
+ "Outputs": {
+ "private_key": {
+ "Description": "The private key if it has been saved.",
+ "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"private_key\"]}"
+ },
+ "public_key": {
+ "Description": "The public key.",
+ "Value": "{\"Fn::GetAtt\": [\"KeyPair\", \"public_key\"]}"
+ }
+ },
+ "Parameters": {
+ "name": {
+ "Description": "The name of the key pair.",
+ "Type": "String"
+ },
+ "public_key": {
+ "Description": "The optional public key. This allows users to supply the public key from a pre-existing key pair. If not supplied, a new key pair will be generated.",
+ "Type": "String"
+ },
+ "save_private_key": {
+ "AllowedValues": [
+ "True",
+ "true",
+ "False",
+ "false"
+ ],
+ "Default": false,
+ "Description": "True if the system should remember a generated private key; False otherwise.",
+ "Type": "String"
+ }
+ },
+ "Resources": {
+ "KeyPair": {
+ "Properties": {
+ "name": {
+ "Ref": "name"
+ },
+ "public_key": {
+ "Ref": "public_key"
+ },
+ "save_private_key": {
+ "Ref": "save_private_key"
+ }
+ },
+ "Type": "OS::Nova::KeyPair"
+ }
+ }
+}`
+
+// HandleGetTemplateSuccessfully creates an HTTP handler at `/resource_types/OS::Heat::AResourceName/template`
+// on the test handler mux that responds with a `Template` response.
+func HandleGetTemplateSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/resource_types/OS::Heat::AResourceName/template", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
diff --git a/openstack/orchestration/v1/stackresources/requests.go b/openstack/orchestration/v1/stackresources/requests.go
new file mode 100644
index 0000000..0ca87b8
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/requests.go
@@ -0,0 +1,132 @@
+package stackresources
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Find retrieves stack resources for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) FindResult {
+ var res FindResult
+
+ // Send request to API
+ _, res.Err = perigee.Request("GET", findURL(c, stackName), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToStackResourceListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Marker and Limit are used for pagination.
+type ListOpts struct {
+ // The stack resource ID with which to start the listing.
+ Marker string `q:"marker"`
+
+ // Integer value for the limit of values to return.
+ Limit int `q:"limit"`
+
+ // Include resources from nest stacks up to Depth levels of recursion.
+ Depth int `q:"nested_depth"`
+}
+
+// ToStackResourceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToStackResourceListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List makes a request against the API to list resources for the given stack.
+func List(client *gophercloud.ServiceClient, stackName, stackID string, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(client, stackName, stackID)
+
+ if opts != nil {
+ query, err := opts.ToStackResourceListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ p := ResourcePage{pagination.MarkerPageBase{PageResult: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ return pagination.NewPager(client, url, createPageFn)
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) GetResult {
+ var res GetResult
+
+ // Send request to API
+ _, res.Err = perigee.Request("GET", getURL(c, stackName, stackID, resourceName), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Metadata retreives the metadata for the given stack resource.
+func Metadata(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) MetadataResult {
+ var res MetadataResult
+
+ // Send request to API
+ _, res.Err = perigee.Request("GET", metadataURL(c, stackName, stackID, resourceName), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// ListTypes makes a request against the API to list resource types.
+func ListTypes(client *gophercloud.ServiceClient) pagination.Pager {
+ url := listTypesURL(client)
+
+ createPageFn := func(r pagination.PageResult) pagination.Page {
+ return ResourceTypePage{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, url, createPageFn)
+}
+
+// Schema retreives the schema for the given resource type.
+func Schema(c *gophercloud.ServiceClient, resourceType string) SchemaResult {
+ var res SchemaResult
+
+ // Send request to API
+ _, res.Err = perigee.Request("GET", schemaURL(c, resourceType), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Template retreives the template representation for the given resource type.
+func Template(c *gophercloud.ServiceClient, resourceType string) TemplateResult {
+ var res TemplateResult
+
+ // Send request to API
+ _, res.Err = perigee.Request("GET", templateURL(c, resourceType), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
diff --git a/openstack/orchestration/v1/stackresources/requests_test.go b/openstack/orchestration/v1/stackresources/requests_test.go
new file mode 100644
index 0000000..f137878
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/requests_test.go
@@ -0,0 +1,107 @@
+package stackresources
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindResources(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleFindSuccessfully(t, FindOutput)
+
+ actual, err := Find(fake.ServiceClient(), "hello_world").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := FindExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResources(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListSuccessfully(t, ListOutput)
+
+ count := 0
+ err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractResources(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ListExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
+
+func TestGetResource(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t, GetOutput)
+
+ actual, err := Get(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := GetExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResourceMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleMetadataSuccessfully(t, MetadataOutput)
+
+ actual, err := Metadata(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := MetadataExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResourceTypes(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListTypesSuccessfully(t, ListTypesOutput)
+
+ count := 0
+ err := ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractResourceTypes(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ListTypesExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGetResourceSchema(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSchemaSuccessfully(t, GetSchemaOutput)
+
+ actual, err := Schema(fake.ServiceClient(), "OS::Heat::AResourceName").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := GetSchemaExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestGetResourceTemplate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetTemplateSuccessfully(t, GetTemplateOutput)
+
+ actual, err := Template(fake.ServiceClient(), "OS::Heat::AResourceName").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := GetTemplateExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/stackresources/results.go b/openstack/orchestration/v1/stackresources/results.go
new file mode 100644
index 0000000..13f5dd2
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/results.go
@@ -0,0 +1,250 @@
+package stackresources
+
+import (
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Resource represents a stack resource.
+type Resource struct {
+ Links []gophercloud.Link `mapstructure:"links"`
+ LogicalID string `mapstructure:"logical_resource_id"`
+ Name string `mapstructure:"resource_name"`
+ PhysicalID string `mapstructure:"physical_resource_id"`
+ RequiredBy []interface{} `mapstructure:"required_by"`
+ Status string `mapstructure:"resource_status"`
+ StatusReason string `mapstructure:"resource_status_reason"`
+ Type string `mapstructure:"resource_type"`
+ UpdatedTime time.Time `mapstructure:"-"`
+}
+
+// FindResult represents the result of a Find operation.
+type FindResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a slice of Resource objects and is called after a
+// Find operation.
+func (r FindResult) Extract() ([]Resource, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Res []Resource `mapstructure:"resources"`
+ }
+
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ resources := r.Body.(map[string]interface{})["resources"].([]interface{})
+
+ for i, resourceRaw := range resources {
+ resource := resourceRaw.(map[string]interface{})
+ if date, ok := resource["updated_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ res.Res[i].UpdatedTime = t
+ }
+ }
+
+ return res.Res, nil
+}
+
+// ResourcePage abstracts the raw results of making a List() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
+// data provided through the ExtractResources call.
+type ResourcePage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a page contains no Server results.
+func (r ResourcePage) IsEmpty() (bool, error) {
+ resources, err := ExtractResources(r)
+ if err != nil {
+ return true, err
+ }
+ return len(resources) == 0, nil
+}
+
+// LastMarker returns the last container name in a ListResult.
+func (r ResourcePage) LastMarker() (string, error) {
+ resources, err := ExtractResources(r)
+ if err != nil {
+ return "", err
+ }
+ if len(resources) == 0 {
+ return "", nil
+ }
+ return resources[len(resources)-1].PhysicalID, nil
+}
+
+// ExtractResources interprets the results of a single page from a List() call, producing a slice of Resource entities.
+func ExtractResources(page pagination.Page) ([]Resource, error) {
+ casted := page.(ResourcePage).Body
+
+ var response struct {
+ Resources []Resource `mapstructure:"resources"`
+ }
+ err := mapstructure.Decode(casted, &response)
+
+ resources := casted.(map[string]interface{})["resources"].([]interface{})
+
+ for i, resourceRaw := range resources {
+ resource := resourceRaw.(map[string]interface{})
+ if date, ok := resource["updated_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ response.Resources[i].UpdatedTime = t
+ }
+ }
+
+ return response.Resources, err
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to a Resource object and is called after a
+// Get operation.
+func (r GetResult) Extract() (*Resource, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Res *Resource `mapstructure:"resource"`
+ }
+
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ resource := r.Body.(map[string]interface{})["resource"].(map[string]interface{})
+
+ if date, ok := resource["updated_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ res.Res.UpdatedTime = t
+ }
+
+ return res.Res, nil
+}
+
+// MetadataResult represents the result of a Metadata operation.
+type MetadataResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a map object and is called after a
+// Metadata operation.
+func (r MetadataResult) Extract() (map[string]string, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Meta map[string]string `mapstructure:"metadata"`
+ }
+
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ return res.Meta, nil
+}
+
+// ResourceTypePage abstracts the raw results of making a ListTypes() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the
+// data provided through the ExtractResourceTypes call.
+type ResourceTypePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ResourceTypePage contains no resource types.
+func (r ResourceTypePage) IsEmpty() (bool, error) {
+ rts, err := ExtractResourceTypes(r)
+ if err != nil {
+ return true, err
+ }
+ return len(rts) == 0, nil
+}
+
+// ExtractResourceTypes extracts and returns resource types.
+func ExtractResourceTypes(page pagination.Page) ([]string, error) {
+ var response struct {
+ ResourceTypes []string `mapstructure:"resource_types"`
+ }
+
+ err := mapstructure.Decode(page.(ResourceTypePage).Body, &response)
+ return response.ResourceTypes, err
+}
+
+// TypeSchema represents a stack resource schema.
+type TypeSchema struct {
+ Attributes map[string]interface{} `mapstructure:"attributes"`
+ Properties map[string]interface{} `mapstrucutre:"properties"`
+ ResourceType string `mapstructure:"resource_type"`
+}
+
+// SchemaResult represents the result of a Schema operation.
+type SchemaResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to a TypeSchema object and is called after a
+// Schema operation.
+func (r SchemaResult) Extract() (*TypeSchema, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res TypeSchema
+
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ return &res, nil
+}
+
+// TypeTemplate represents a stack resource template.
+type TypeTemplate struct {
+ HeatTemplateFormatVersion string
+ Outputs map[string]interface{}
+ Parameters map[string]interface{}
+ Resources map[string]interface{}
+}
+
+// TemplateResult represents the result of a Template operation.
+type TemplateResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to a TypeTemplate object and is called after a
+// Template operation.
+func (r TemplateResult) Extract() (*TypeTemplate, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res TypeTemplate
+
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ return &res, nil
+}
diff --git a/openstack/orchestration/v1/stackresources/urls.go b/openstack/orchestration/v1/stackresources/urls.go
new file mode 100644
index 0000000..ef078d9
--- /dev/null
+++ b/openstack/orchestration/v1/stackresources/urls.go
@@ -0,0 +1,31 @@
+package stackresources
+
+import "github.com/rackspace/gophercloud"
+
+func findURL(c *gophercloud.ServiceClient, stackName string) string {
+ return c.ServiceURL("stacks", stackName, "resources")
+}
+
+func listURL(c *gophercloud.ServiceClient, stackName, stackID string) string {
+ return c.ServiceURL("stacks", stackName, stackID, "resources")
+}
+
+func getURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string {
+ return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName)
+}
+
+func metadataURL(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) string {
+ return c.ServiceURL("stacks", stackName, stackID, "resources", resourceName, "metadata")
+}
+
+func listTypesURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("resource_types")
+}
+
+func schemaURL(c *gophercloud.ServiceClient, typeName string) string {
+ return c.ServiceURL("resource_types", typeName)
+}
+
+func templateURL(c *gophercloud.ServiceClient, typeName string) string {
+ return c.ServiceURL("resource_types", typeName, "template")
+}
diff --git a/openstack/orchestration/v1/stacks/doc.go b/openstack/orchestration/v1/stacks/doc.go
new file mode 100644
index 0000000..19231b5
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/doc.go
@@ -0,0 +1,8 @@
+// Package stacks provides operation for working with Heat stacks. A stack is a
+// group of resources (servers, load balancers, databases, and so forth)
+// combined to fulfill a useful purpose. Based on a template, Heat orchestration
+// engine creates an instantiated set of resources (a stack) to run the
+// application framework or component specified (in the template). A stack is a
+// running instance of a template. The result of creating a stack is a deployment
+// of the application framework or component.
+package stacks
diff --git a/openstack/orchestration/v1/stacks/fixtures.go b/openstack/orchestration/v1/stacks/fixtures.go
new file mode 100644
index 0000000..6d3e959
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/fixtures.go
@@ -0,0 +1,374 @@
+package stacks
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// CreateExpected represents the expected object from a Create request.
+var CreateExpected = &CreatedStack{
+ ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ Rel: "self",
+ },
+ },
+}
+
+// CreateOutput represents the response body from a Create request.
+const CreateOutput = `
+{
+ "stack": {
+ "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ "links": [
+ {
+ "href": "http://168.28.170.117:8004/v1/98606384f58drad0bhdb7d02779549ac/stacks/stackcreated/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ "rel": "self"
+ }
+ ]
+ }
+}`
+
+// HandleCreateSuccessfully creates an HTTP handler at `/stacks` on the test handler mux
+// that responds with a `Create` response.
+func HandleCreateSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks", 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, "Accept", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// ListExpected represents the expected object from a List request.
+var ListExpected = []ListedStack{
+ ListedStack{
+ Description: "Simple template to test heat commands",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ Rel: "self",
+ },
+ },
+ StatusReason: "Stack CREATE completed successfully",
+ Name: "postman_stack",
+ CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC),
+ Status: "CREATE_COMPLETE",
+ ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ },
+ ListedStack{
+ Description: "Simple template to test heat commands",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada",
+ Rel: "self",
+ },
+ },
+ StatusReason: "Stack successfully updated",
+ Name: "gophercloud-test-stack-2",
+ CreationTime: time.Date(2014, 12, 11, 17, 39, 16, 0, time.UTC),
+ UpdatedTime: time.Date(2014, 12, 11, 17, 40, 37, 0, time.UTC),
+ Status: "UPDATE_COMPLETE",
+ ID: "db6977b2-27aa-4775-9ae7-6213212d4ada",
+ },
+}
+
+// FullListOutput represents the response body from a List request without a marker.
+const FullListOutput = `
+{
+ "stacks": [
+ {
+ "description": "Simple template to test heat commands",
+ "links": [
+ {
+ "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ "rel": "self"
+ }
+ ],
+ "stack_status_reason": "Stack CREATE completed successfully",
+ "stack_name": "postman_stack",
+ "creation_time": "2015-02-03T20:07:39Z",
+ "updated_time": null,
+ "stack_status": "CREATE_COMPLETE",
+ "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87"
+ },
+ {
+ "description": "Simple template to test heat commands",
+ "links": [
+ {
+ "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada",
+ "rel": "self"
+ }
+ ],
+ "stack_status_reason": "Stack successfully updated",
+ "stack_name": "gophercloud-test-stack-2",
+ "creation_time": "2014-12-11T17:39:16Z",
+ "updated_time": "2014-12-11T17:40:37Z",
+ "stack_status": "UPDATE_COMPLETE",
+ "id": "db6977b2-27aa-4775-9ae7-6213212d4ada"
+ }
+ ]
+}
+`
+
+// HandleListSuccessfully creates an HTTP handler at `/stacks` on the test handler mux
+// that responds with a `List` response.
+func HandleListSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, output)
+ case "db6977b2-27aa-4775-9ae7-6213212d4ada":
+ fmt.Fprintf(w, `[]`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+}
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &RetrievedStack{
+ DisableRollback: true,
+ Description: "Simple template to test heat commands",
+ Parameters: map[string]string{
+ "flavor": "m1.tiny",
+ "OS::stack_name": "postman_stack",
+ "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ },
+ StatusReason: "Stack CREATE completed successfully",
+ Name: "postman_stack",
+ Outputs: []map[string]interface{}{},
+ CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC),
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ Rel: "self",
+ },
+ },
+ Capabilities: []interface{}{},
+ NotificationTopics: []interface{}{},
+ Status: "CREATE_COMPLETE",
+ ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ TemplateDescription: "Simple template to test heat commands",
+}
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+ "stack": {
+ "disable_rollback": true,
+ "description": "Simple template to test heat commands",
+ "parameters": {
+ "flavor": "m1.tiny",
+ "OS::stack_name": "postman_stack",
+ "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87"
+ },
+ "stack_status_reason": "Stack CREATE completed successfully",
+ "stack_name": "postman_stack",
+ "outputs": [],
+ "creation_time": "2015-02-03T20:07:39Z",
+ "links": [
+ {
+ "href": "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ "rel": "self"
+ }
+ ],
+ "capabilities": [],
+ "notification_topics": [],
+ "timeout_mins": null,
+ "stack_status": "CREATE_COMPLETE",
+ "updated_time": null,
+ "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ "template_description": "Simple template to test heat commands"
+ }
+}
+`
+
+// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// HandleUpdateSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87`
+// on the test handler mux that responds with an `Update` response.
+func HandleUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
+
+// HandleDeleteSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87`
+// on the test handler mux that responds with a `Delete` response.
+func HandleDeleteSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/stacks/gophercloud-test-stack-2/db6977b2-27aa-4775-9ae7-6213212d4ada", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// GetExpected represents the expected object from a Get request.
+var PreviewExpected = &PreviewedStack{
+ DisableRollback: true,
+ Description: "Simple template to test heat commands",
+ Parameters: map[string]string{
+ "flavor": "m1.tiny",
+ "OS::stack_name": "postman_stack",
+ "OS::stack_id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ },
+ StatusReason: "Stack CREATE completed successfully",
+ Name: "postman_stack",
+ CreationTime: time.Date(2015, 2, 3, 20, 7, 39, 0, time.UTC),
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "http://166.76.160.117:8004/v1/98606384f58d4ad0b3db7d0d779549ac/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ Rel: "self",
+ },
+ },
+ Capabilities: []interface{}{},
+ NotificationTopics: []interface{}{},
+ Status: "CREATE_COMPLETE",
+ ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ TemplateDescription: "Simple template to test heat commands",
+}
+
+// HandlePreviewSuccessfully creates an HTTP handler at `/stacks/preview`
+// on the test handler mux that responds with a `Preview` response.
+func HandlePreviewSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/preview", 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, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// AbandonExpected represents the expected object from an Abandon request.
+var AbandonExpected = &AbandonedStack{
+ Status: "COMPLETE",
+ Name: "postman_stack",
+ Template: map[string]interface{}{
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": map[string]interface{}{
+ "flavor": map[string]interface{}{
+ "default": "m1.tiny",
+ "type": "string",
+ },
+ },
+ "resources": map[string]interface{}{
+ "hello_world": map[string]interface{}{
+ "type": "OS::Nova::Server",
+ "properties": map[string]interface{}{
+ "key_name": "heat_key",
+ "flavor": map[string]interface{}{
+ "get_param": "flavor",
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n",
+ },
+ },
+ },
+ },
+ Action: "CREATE",
+ ID: "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ Resources: map[string]interface{}{
+ "hello_world": map[string]interface{}{
+ "status": "COMPLETE",
+ "name": "hello_world",
+ "resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63",
+ "action": "CREATE",
+ "type": "OS::Nova::Server",
+ },
+ },
+}
+
+// AbandonOutput represents the response body from an Abandon request.
+const AbandonOutput = `
+{
+ "status": "COMPLETE",
+ "name": "postman_stack",
+ "template": {
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {
+ "flavor": {
+ "default": "m1.tiny",
+ "type": "string"
+ }
+ },
+ "resources": {
+ "hello_world": {
+ "type": "OS::Nova::Server",
+ "properties": {
+ "key_name": "heat_key",
+ "flavor": {
+ "get_param": "flavor"
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+ },
+ "action": "CREATE",
+ "id": "16ef0584-4458-41eb-87c8-0dc8d5f66c87",
+ "resources": {
+ "hello_world": {
+ "status": "COMPLETE",
+ "name": "hello_world",
+ "resource_id": "8a310d36-46fc-436f-8be4-37a696b8ac63",
+ "action": "CREATE",
+ "type": "OS::Nova::Server",
+ }
+ }
+}`
+
+// HandleAbandonSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon`
+// on the test handler mux that responds with an `Abandon` response.
+func HandleAbandonSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/abandon", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, AbandonOutput)
+ })
+}
diff --git a/openstack/orchestration/v1/stacks/requests.go b/openstack/orchestration/v1/stacks/requests.go
new file mode 100644
index 0000000..549aecf
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/requests.go
@@ -0,0 +1,528 @@
+package stacks
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Rollback is used to specify whether or not a stack can be rolled back.
+type Rollback *bool
+
+var (
+ disable = true
+ // Disable is used to specify that a stack cannot be rolled back.
+ Disable Rollback = &disable
+ enable = false
+ // Enable is used to specify that a stack can be rolled back.
+ Enable Rollback = &enable
+)
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToStackCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // (REQUIRED) The name of the stack. It must start with an alphabetic character.
+ Name string
+ // (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
+ // This value is ignored if Template is supplied inline.
+ TemplateURL string
+ // (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
+ // is a stringified version of the JSON/YAML template. Since the template will likely
+ // be located in a file, one way to set this variable is by using ioutil.ReadFile:
+ // import "io/ioutil"
+ // var opts stacks.CreateOpts
+ // b, err := ioutil.ReadFile("path/to/you/template/file.json")
+ // if err != nil {
+ // // handle error...
+ // }
+ // opts.Template = string(b)
+ Template string
+ // (OPTIONAL) Enables or disables deletion of all stack resources when a stack
+ // creation fails. Default is true, meaning all resources are not deleted when
+ // stack creation fails.
+ DisableRollback Rollback
+ // (OPTIONAL) A stringified JSON environment for the stack.
+ Environment string
+ // (OPTIONAL) A map that maps file names to file contents. It can also be used
+ // to pass provider template contents. Example:
+ // Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
+ Files map[string]interface{}
+ // (OPTIONAL) User-defined parameters to pass to the template.
+ Parameters map[string]string
+ // (OPTIONAL) The timeout for stack creation in minutes.
+ Timeout int
+}
+
+// ToStackCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToStackCreateMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.Name == "" {
+ return s, errors.New("Required field 'Name' not provided.")
+ }
+ s["stack_name"] = opts.Name
+
+ if opts.Template != "" {
+ s["template"] = opts.Template
+ } else if opts.TemplateURL != "" {
+ s["template_url"] = opts.TemplateURL
+ } else {
+ return s, errors.New("Either Template or TemplateURL must be provided.")
+ }
+
+ if opts.DisableRollback != nil {
+ s["disable_rollback"] = &opts.DisableRollback
+ }
+
+ if opts.Environment != "" {
+ s["environment"] = opts.Environment
+ }
+ if opts.Files != nil {
+ s["files"] = opts.Files
+ }
+ if opts.Parameters != nil {
+ s["parameters"] = opts.Parameters
+ }
+
+ if opts.Timeout != 0 {
+ s["timeout_mins"] = opts.Timeout
+ }
+
+ return s, nil
+}
+
+// Create accepts a CreateOpts struct and creates a new stack using the values
+// provided.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToStackCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// AdoptOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the Adopt function in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type AdoptOptsBuilder interface {
+ ToStackAdoptMap() (map[string]interface{}, error)
+}
+
+// AdoptOpts is the common options struct used in this package's Adopt
+// operation.
+type AdoptOpts struct {
+ // (REQUIRED) Existing resources data represented as a string to add to the
+ // new stack. Data returned by Abandon could be provided as AdoptsStackData.
+ AdoptStackData string
+ // (REQUIRED) The name of the stack. It must start with an alphabetic character.
+ Name string
+ // (REQUIRED) The timeout for stack creation in minutes.
+ Timeout int
+ // (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
+ // This value is ignored if Template is supplied inline.
+ TemplateURL string
+ // (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
+ // is a stringified version of the JSON/YAML template. Since the template will likely
+ // be located in a file, one way to set this variable is by using ioutil.ReadFile:
+ // import "io/ioutil"
+ // var opts stacks.CreateOpts
+ // b, err := ioutil.ReadFile("path/to/you/template/file.json")
+ // if err != nil {
+ // // handle error...
+ // }
+ // opts.Template = string(b)
+ Template string
+ // (OPTIONAL) Enables or disables deletion of all stack resources when a stack
+ // creation fails. Default is true, meaning all resources are not deleted when
+ // stack creation fails.
+ DisableRollback Rollback
+ // (OPTIONAL) A stringified JSON environment for the stack.
+ Environment string
+ // (OPTIONAL) A map that maps file names to file contents. It can also be used
+ // to pass provider template contents. Example:
+ // Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
+ Files map[string]interface{}
+ // (OPTIONAL) User-defined parameters to pass to the template.
+ Parameters map[string]string
+}
+
+// ToStackAdoptMap casts a CreateOpts struct to a map.
+func (opts AdoptOpts) ToStackAdoptMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.Name == "" {
+ return s, errors.New("Required field 'Name' not provided.")
+ }
+ s["stack_name"] = opts.Name
+
+ if opts.Template != "" {
+ s["template"] = opts.Template
+ } else if opts.TemplateURL != "" {
+ s["template_url"] = opts.TemplateURL
+ } else {
+ return s, errors.New("Either Template or TemplateURL must be provided.")
+ }
+
+ if opts.AdoptStackData == "" {
+ return s, errors.New("Required field 'AdoptStackData' not provided.")
+ }
+ s["adopt_stack_data"] = opts.AdoptStackData
+
+ if opts.DisableRollback != nil {
+ s["disable_rollback"] = &opts.DisableRollback
+ }
+
+ if opts.Environment != "" {
+ s["environment"] = opts.Environment
+ }
+ if opts.Files != nil {
+ s["files"] = opts.Files
+ }
+ if opts.Parameters != nil {
+ s["parameters"] = opts.Parameters
+ }
+
+ if opts.Timeout == 0 {
+ return nil, errors.New("Required field 'Timeout' not provided.")
+ }
+ s["timeout_mins"] = opts.Timeout
+
+ return map[string]interface{}{"stack": s}, nil
+}
+
+// Adopt accepts an AdoptOpts struct and creates a new stack using the resources
+// from another stack.
+func Adopt(c *gophercloud.ServiceClient, opts AdoptOptsBuilder) AdoptResult {
+ var res AdoptResult
+
+ reqBody, err := opts.ToStackAdoptMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = perigee.Request("POST", adoptURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// SortDir is a type for specifying in which direction to sort a list of stacks.
+type SortDir string
+
+// SortKey is a type for specifying by which key to sort a list of stacks.
+type SortKey string
+
+var (
+ // SortAsc is used to sort a list of stacks in ascending order.
+ SortAsc SortDir = "asc"
+ // SortDesc is used to sort a list of stacks in descending order.
+ SortDesc SortDir = "desc"
+ // SortName is used to sort a list of stacks by name.
+ SortName SortKey = "name"
+ // SortStatus is used to sort a list of stacks by status.
+ SortStatus SortKey = "status"
+ // SortCreatedAt is used to sort a list of stacks by date created.
+ SortCreatedAt SortKey = "created_at"
+ // SortUpdatedAt is used to sort a list of stacks by date updated.
+ SortUpdatedAt SortKey = "updated_at"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToStackListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the network attributes you want to see returned. SortKey allows you to sort
+// by a particular network attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Status string `q:"status"`
+ Name string `q:"name"`
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+ SortKey SortKey `q:"sort_keys"`
+ SortDir SortDir `q:"sort_dir"`
+}
+
+// ToStackListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToStackListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// stacks. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToStackListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ createPage := func(r pagination.PageResult) pagination.Page {
+ return StackPage{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(c, url, createPage)
+}
+
+// Get retreives a stack based on the stack name and stack ID.
+func Get(c *gophercloud.ServiceClient, stackName, stackID string) GetResult {
+ var res GetResult
+
+ // Send request to API
+ _, res.Err = perigee.Request("GET", getURL(c, stackName, stackID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the Update operation in this package.
+type UpdateOptsBuilder interface {
+ ToStackUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+ // (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
+ // This value is ignored if Template is supplied inline.
+ TemplateURL string
+ // (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
+ // is a stringified version of the JSON/YAML template. Since the template will likely
+ // be located in a file, one way to set this variable is by using ioutil.ReadFile:
+ // import "io/ioutil"
+ // var opts stacks.CreateOpts
+ // b, err := ioutil.ReadFile("path/to/you/template/file.json")
+ // if err != nil {
+ // // handle error...
+ // }
+ // opts.Template = string(b)
+ Template string
+ // (OPTIONAL) A stringified JSON environment for the stack.
+ Environment string
+ // (OPTIONAL) A map that maps file names to file contents. It can also be used
+ // to pass provider template contents. Example:
+ // Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
+ Files map[string]interface{}
+ // (OPTIONAL) User-defined parameters to pass to the template.
+ Parameters map[string]string
+ // (OPTIONAL) The timeout for stack creation in minutes.
+ Timeout int
+}
+
+// ToStackUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToStackUpdateMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.Template != "" {
+ s["template"] = opts.Template
+ } else if opts.TemplateURL != "" {
+ s["template_url"] = opts.TemplateURL
+ } else {
+ return s, errors.New("Either Template or TemplateURL must be provided.")
+ }
+
+ if opts.Environment != "" {
+ s["environment"] = opts.Environment
+ }
+
+ if opts.Files != nil {
+ s["files"] = opts.Files
+ }
+
+ if opts.Parameters != nil {
+ s["parameters"] = opts.Parameters
+ }
+
+ if opts.Timeout != 0 {
+ s["timeout_mins"] = opts.Timeout
+ }
+
+ return s, nil
+}
+
+// Update accepts an UpdateOpts struct and updates an existing stack using the values
+// provided.
+func Update(c *gophercloud.ServiceClient, stackName, stackID string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
+
+ reqBody, err := opts.ToStackUpdateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = perigee.Request("PUT", updateURL(c, stackName, stackID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+ return res
+}
+
+// Delete deletes a stack based on the stack name and stack ID.
+func Delete(c *gophercloud.ServiceClient, stackName, stackID string) DeleteResult {
+ var res DeleteResult
+
+ // Send request to API
+ _, res.Err = perigee.Request("DELETE", deleteURL(c, stackName, stackID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
+
+// PreviewOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the Preview operation in this package.
+type PreviewOptsBuilder interface {
+ ToStackPreviewMap() (map[string]interface{}, error)
+}
+
+// PreviewOpts contains the common options struct used in this package's Preview
+// operation.
+type PreviewOpts struct {
+ // (REQUIRED) The name of the stack. It must start with an alphabetic character.
+ Name string
+ // (REQUIRED) The timeout for stack creation in minutes.
+ Timeout int
+ // (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
+ // This value is ignored if Template is supplied inline.
+ TemplateURL string
+ // (OPTIONAL; REQUIRED IF TemplateURL IS EMPTY) A template to instantiate. The value
+ // is a stringified version of the JSON/YAML template. Since the template will likely
+ // be located in a file, one way to set this variable is by using ioutil.ReadFile:
+ // import "io/ioutil"
+ // var opts stacks.CreateOpts
+ // b, err := ioutil.ReadFile("path/to/you/template/file.json")
+ // if err != nil {
+ // // handle error...
+ // }
+ // opts.Template = string(b)
+ Template string
+ // (OPTIONAL) Enables or disables deletion of all stack resources when a stack
+ // creation fails. Default is true, meaning all resources are not deleted when
+ // stack creation fails.
+ DisableRollback Rollback
+ // (OPTIONAL) A stringified JSON environment for the stack.
+ Environment string
+ // (OPTIONAL) A map that maps file names to file contents. It can also be used
+ // to pass provider template contents. Example:
+ // Files: `{"myfile": "#!/bin/bash\necho 'Hello world' > /root/testfile.txt"}`
+ Files map[string]interface{}
+ // (OPTIONAL) User-defined parameters to pass to the template.
+ Parameters map[string]string
+}
+
+// ToStackPreviewMap casts a PreviewOpts struct to a map.
+func (opts PreviewOpts) ToStackPreviewMap() (map[string]interface{}, error) {
+ s := make(map[string]interface{})
+
+ if opts.Name == "" {
+ return s, errors.New("Required field 'Name' not provided.")
+ }
+ s["stack_name"] = opts.Name
+
+ if opts.Template != "" {
+ s["template"] = opts.Template
+ } else if opts.TemplateURL != "" {
+ s["template_url"] = opts.TemplateURL
+ } else {
+ return s, errors.New("Either Template or TemplateURL must be provided.")
+ }
+
+ if opts.DisableRollback != nil {
+ s["disable_rollback"] = &opts.DisableRollback
+ }
+
+ if opts.Environment != "" {
+ s["environment"] = opts.Environment
+ }
+ if opts.Files != nil {
+ s["files"] = opts.Files
+ }
+ if opts.Parameters != nil {
+ s["parameters"] = opts.Parameters
+ }
+
+ if opts.Timeout != 0 {
+ s["timeout_mins"] = opts.Timeout
+ }
+
+ return s, nil
+}
+
+// Preview accepts a PreviewOptsBuilder interface and creates a preview of a stack using the values
+// provided.
+func Preview(c *gophercloud.ServiceClient, opts PreviewOptsBuilder) PreviewResult {
+ var res PreviewResult
+
+ reqBody, err := opts.ToStackPreviewMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ // Send request to API
+ _, res.Err = perigee.Request("POST", previewURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Abandon deletes the stack with the provided stackName and stackID, but leaves its
+// resources intact, and returns data describing the stack and its resources.
+func Abandon(c *gophercloud.ServiceClient, stackName, stackID string) AbandonResult {
+ var res AbandonResult
+
+ // Send request to API
+ _, res.Err = perigee.Request("DELETE", abandonURL(c, stackName, stackID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
diff --git a/openstack/orchestration/v1/stacks/requests_test.go b/openstack/orchestration/v1/stacks/requests_test.go
new file mode 100644
index 0000000..1e32ca2
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/requests_test.go
@@ -0,0 +1,217 @@
+package stacks
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreateStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleCreateSuccessfully(t, CreateOutput)
+
+ createOpts := CreateOpts{
+ Name: "stackcreated",
+ Timeout: 60,
+ Template: `
+ {
+ "stack_name": "postman_stack",
+ "template": {
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {
+ "flavor": {
+ "default": "m1.tiny",
+ "type": "string"
+ }
+ },
+ "resources": {
+ "hello_world": {
+ "type":"OS::Nova::Server",
+ "properties": {
+ "key_name": "heat_key",
+ "flavor": {
+ "get_param": "flavor"
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+ }
+ }`,
+ DisableRollback: Disable,
+ }
+ actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := CreateExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestAdoptStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleCreateSuccessfully(t, CreateOutput)
+
+ adoptOpts := AdoptOpts{
+ AdoptStackData: `{environment{parameters{}}}`,
+ Name: "stackcreated",
+ Timeout: 60,
+ Template: `
+ {
+ "stack_name": "postman_stack",
+ "template": {
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {
+ "flavor": {
+ "default": "m1.tiny",
+ "type": "string"
+ }
+ },
+ "resources": {
+ "hello_world": {
+ "type":"OS::Nova::Server",
+ "properties": {
+ "key_name": "heat_key",
+ "flavor": {
+ "get_param": "flavor"
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+ }
+ }`,
+ DisableRollback: Disable,
+ }
+ actual, err := Adopt(fake.ServiceClient(), adoptOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := CreateExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListSuccessfully(t, FullListOutput)
+
+ count := 0
+ err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractStacks(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ListExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
+
+func TestGetStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t, GetOutput)
+
+ actual, err := Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := GetExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestUpdateStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleUpdateSuccessfully(t)
+
+ updateOpts := UpdateOpts{
+ Template: `
+ {
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {
+ "flavor": {
+ "default": "m1.tiny",
+ "type": "string"
+ }
+ },
+ "resources": {
+ "hello_world": {
+ "type":"OS::Nova::Server",
+ "properties": {
+ "key_name": "heat_key",
+ "flavor": {
+ "get_param": "flavor"
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+ }`,
+ }
+ err := Update(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDeleteStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleDeleteSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada").ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestPreviewStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePreviewSuccessfully(t, GetOutput)
+
+ previewOpts := PreviewOpts{
+ Name: "stackcreated",
+ Timeout: 60,
+ Template: `
+ {
+ "stack_name": "postman_stack",
+ "template": {
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {
+ "flavor": {
+ "default": "m1.tiny",
+ "type": "string"
+ }
+ },
+ "resources": {
+ "hello_world": {
+ "type":"OS::Nova::Server",
+ "properties": {
+ "key_name": "heat_key",
+ "flavor": {
+ "get_param": "flavor"
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+ }
+ }`,
+ DisableRollback: Disable,
+ }
+ actual, err := Preview(fake.ServiceClient(), previewOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := PreviewExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/stacks/results.go b/openstack/orchestration/v1/stacks/results.go
new file mode 100644
index 0000000..ff971e8
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/results.go
@@ -0,0 +1,296 @@
+package stacks
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// CreatedStack represents the object extracted from a Create operation.
+type CreatedStack struct {
+ ID string `mapstructure:"id"`
+ Links []gophercloud.Link `mapstructure:"links"`
+}
+
+// CreateResult represents the result of a Create operation.
+type CreateResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to a CreatedStack object and is called after a
+// Create operation.
+func (r CreateResult) Extract() (*CreatedStack, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Stack *CreatedStack `mapstructure:"stack"`
+ }
+
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ return res.Stack, nil
+}
+
+// AdoptResult represents the result of an Adopt operation. AdoptResult has the
+// same form as CreateResult.
+type AdoptResult struct {
+ CreateResult
+}
+
+// StackPage is a pagination.Pager that is returned from a call to the List function.
+type StackPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Stacks.
+func (r StackPage) IsEmpty() (bool, error) {
+ stacks, err := ExtractStacks(r)
+ if err != nil {
+ return true, err
+ }
+ return len(stacks) == 0, nil
+}
+
+// ListedStack represents an element in the slice extracted from a List operation.
+type ListedStack struct {
+ CreationTime time.Time `mapstructure:"-"`
+ Description string `mapstructure:"description"`
+ ID string `mapstructure:"id"`
+ Links []gophercloud.Link `mapstructure:"links"`
+ Name string `mapstructure:"stack_name"`
+ Status string `mapstructure:"stack_status"`
+ StatusReason string `mapstructure:"stack_status_reason"`
+ UpdatedTime time.Time `mapstructure:"-"`
+}
+
+// ExtractStacks extracts and returns a slice of ListedStack. It is used while iterating
+// over a stacks.List call.
+func ExtractStacks(page pagination.Page) ([]ListedStack, error) {
+ var res struct {
+ Stacks []ListedStack `mapstructure:"stacks"`
+ }
+
+ err := mapstructure.Decode(page.(StackPage).Body, &res)
+ if err != nil {
+ return nil, err
+ }
+
+ rawStacks := (((page.(StackPage).Body).(map[string]interface{}))["stacks"]).([]interface{})
+ for i := range rawStacks {
+ thisStack := (rawStacks[i]).(map[string]interface{})
+
+ if t, ok := thisStack["creation_time"].(string); ok && t != "" {
+ creationTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return res.Stacks, err
+ }
+ res.Stacks[i].CreationTime = creationTime
+ }
+
+ if t, ok := thisStack["updated_time"].(string); ok && t != "" {
+ updatedTime, err := time.Parse(time.RFC3339, t)
+ if err != nil {
+ return res.Stacks, err
+ }
+ res.Stacks[i].UpdatedTime = updatedTime
+ }
+ }
+
+ return res.Stacks, nil
+}
+
+// RetrievedStack represents the object extracted from a Get operation.
+type RetrievedStack struct {
+ Capabilities []interface{} `mapstructure:"capabilities"`
+ CreationTime time.Time `mapstructure:"-"`
+ Description string `mapstructure:"description"`
+ DisableRollback bool `mapstructure:"disable_rollback"`
+ ID string `mapstructure:"id"`
+ Links []gophercloud.Link `mapstructure:"links"`
+ NotificationTopics []interface{} `mapstructure:"notification_topics"`
+ Outputs []map[string]interface{} `mapstructure:"outputs"`
+ Parameters map[string]string `mapstructure:"parameters"`
+ Name string `mapstructure:"stack_name"`
+ Status string `mapstructure:"stack_status"`
+ StatusReason string `mapstructure:"stack_status_reason"`
+ TemplateDescription string `mapstructure:"template_description"`
+ Timeout int `mapstructure:"timeout_mins"`
+ UpdatedTime time.Time `mapstructure:"-"`
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to a RetrievedStack object and is called after a
+// Get operation.
+func (r GetResult) Extract() (*RetrievedStack, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Stack *RetrievedStack `mapstructure:"stack"`
+ }
+
+ config := &mapstructure.DecoderConfig{
+ Result: &res,
+ WeaklyTypedInput: true,
+ }
+ decoder, err := mapstructure.NewDecoder(config)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := decoder.Decode(r.Body); err != nil {
+ return nil, err
+ }
+
+ b := r.Body.(map[string]interface{})["stack"].(map[string]interface{})
+
+ if date, ok := b["creation_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ res.Stack.CreationTime = t
+ }
+
+ if date, ok := b["updated_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ res.Stack.UpdatedTime = t
+ }
+
+ return res.Stack, err
+}
+
+// UpdateResult represents the result of a Update operation.
+type UpdateResult struct {
+ gophercloud.ErrResult
+}
+
+// DeleteResult represents the result of a Delete operation.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// PreviewedStack represents the result of a Preview operation.
+type PreviewedStack struct {
+ Capabilities []interface{} `mapstructure:"capabilities"`
+ CreationTime time.Time `mapstructure:"-"`
+ Description string `mapstructure:"description"`
+ DisableRollback bool `mapstructure:"disable_rollback"`
+ ID string `mapstructure:"id"`
+ Links []gophercloud.Link `mapstructure:"links"`
+ Name string `mapstructure:"stack_name"`
+ NotificationTopics []interface{} `mapstructure:"notification_topics"`
+ Parameters map[string]string `mapstructure:"parameters"`
+ Resources []map[string]interface{} `mapstructure:"resources"`
+ Status string `mapstructure:"stack_status"`
+ StatusReason string `mapstructure:"stack_status_reason"`
+ TemplateDescription string `mapstructure:"template_description"`
+ Timeout int `mapstructure:"timeout_mins"`
+ UpdatedTime time.Time `mapstructure:"-"`
+}
+
+// PreviewResult represents the result of a Preview operation.
+type PreviewResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to a PreviewedStack object and is called after a
+// Preview operation.
+func (r PreviewResult) Extract() (*PreviewedStack, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Stack *PreviewedStack `mapstructure:"stack"`
+ }
+
+ config := &mapstructure.DecoderConfig{
+ Result: &res,
+ WeaklyTypedInput: true,
+ }
+ decoder, err := mapstructure.NewDecoder(config)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := decoder.Decode(r.Body); err != nil {
+ return nil, err
+ }
+
+ b := r.Body.(map[string]interface{})["stack"].(map[string]interface{})
+
+ if date, ok := b["creation_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ res.Stack.CreationTime = t
+ }
+
+ if date, ok := b["updated_time"]; ok && date != nil {
+ t, err := time.Parse(time.RFC3339, date.(string))
+ if err != nil {
+ return nil, err
+ }
+ res.Stack.UpdatedTime = t
+ }
+
+ return res.Stack, err
+}
+
+// AbandonedStack represents the result of an Abandon operation.
+type AbandonedStack struct {
+ Status string `mapstructure:"status"`
+ Name string `mapstructure:"name"`
+ Template map[string]interface{} `mapstructure:"template"`
+ Action string `mapstructure:"action"`
+ ID string `mapstructure:"id"`
+ Resources map[string]interface{} `mapstructure:"resources"`
+}
+
+// AbandonResult represents the result of an Abandon operation.
+type AbandonResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to an AbandonedStack object and is called after an
+// Abandon operation.
+func (r AbandonResult) Extract() (*AbandonedStack, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res AbandonedStack
+
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ return &res, nil
+}
+
+// String converts an AbandonResult to a string. This is useful to when passing
+// the result of an Abandon operation to an AdoptOpts AdoptStackData field.
+func (r AbandonResult) String() (string, error) {
+ out, err := json.Marshal(r)
+ if err != nil {
+ return "", err
+ }
+ return string(out), nil
+}
diff --git a/openstack/orchestration/v1/stacks/urls.go b/openstack/orchestration/v1/stacks/urls.go
new file mode 100644
index 0000000..3dd2bb3
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/urls.go
@@ -0,0 +1,35 @@
+package stacks
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("stacks")
+}
+
+func adoptURL(c *gophercloud.ServiceClient) string {
+ return createURL(c)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return createURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, name, id string) string {
+ return c.ServiceURL("stacks", name, id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, name, id string) string {
+ return getURL(c, name, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, name, id string) string {
+ return getURL(c, name, id)
+}
+
+func previewURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("stacks", "preview")
+}
+
+func abandonURL(c *gophercloud.ServiceClient, name, id string) string {
+ return c.ServiceURL("stacks", name, id, "abandon")
+}
diff --git a/openstack/orchestration/v1/stacktemplates/doc.go b/openstack/orchestration/v1/stacktemplates/doc.go
new file mode 100644
index 0000000..5af0bd6
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/doc.go
@@ -0,0 +1,8 @@
+// Package stacktemplates provides operations for working with Heat templates.
+// A Cloud Orchestration template is a portable file, written in a user-readable
+// language, that describes how a set of resources should be assembled and what
+// software should be installed in order to produce a working stack. The template
+// specifies what resources should be used, what attributes can be set, and other
+// parameters that are critical to the successful, repeatable automation of a
+// specific application stack.
+package stacktemplates
diff --git a/openstack/orchestration/v1/stacktemplates/fixtures.go b/openstack/orchestration/v1/stacktemplates/fixtures.go
new file mode 100644
index 0000000..71fa808
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/fixtures.go
@@ -0,0 +1,118 @@
+package stacktemplates
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// GetExpected represents the expected object from a Get request.
+var GetExpected = &Template{
+ Description: "Simple template to test heat commands",
+ HeatTemplateVersion: "2013-05-23",
+ Parameters: map[string]interface{}{
+ "flavor": map[string]interface{}{
+ "default": "m1.tiny",
+ "type": "string",
+ },
+ },
+ Resources: map[string]interface{}{
+ "hello_world": map[string]interface{}{
+ "type": "OS::Nova::Server",
+ "properties": map[string]interface{}{
+ "key_name": "heat_key",
+ "flavor": map[string]interface{}{
+ "get_param": "flavor",
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n",
+ },
+ },
+ },
+}
+
+// GetOutput represents the response body from a Get request.
+const GetOutput = `
+{
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {
+ "flavor": {
+ "default": "m1.tiny",
+ "type": "string"
+ }
+ },
+ "resources": {
+ "hello_world": {
+ "type": "OS::Nova::Server",
+ "properties": {
+ "key_name": "heat_key",
+ "flavor": {
+ "get_param": "flavor"
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+}`
+
+// HandleGetSuccessfully creates an HTTP handler at `/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template`
+// on the test handler mux that responds with a `Get` response.
+func HandleGetSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/stacks/postman_stack/16ef0584-4458-41eb-87c8-0dc8d5f66c87/template", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
+
+// ValidateExpected represents the expected object from a Validate request.
+var ValidateExpected = &ValidatedTemplate{
+ Description: "Simple template to test heat commands",
+ Parameters: map[string]interface{}{
+ "flavor": map[string]interface{}{
+ "Default": "m1.tiny",
+ "Type": "String",
+ "NoEcho": "false",
+ "Description": "",
+ "Label": "flavor",
+ },
+ },
+}
+
+// ValidateOutput represents the response body from a Validate request.
+const ValidateOutput = `
+{
+ "Description": "Simple template to test heat commands",
+ "Parameters": {
+ "flavor": {
+ "Default": "m1.tiny",
+ "Type": "String",
+ "NoEcho": "false",
+ "Description": "",
+ "Label": "flavor"
+ }
+ }
+}`
+
+// HandleValidateSuccessfully creates an HTTP handler at `/validate`
+// on the test handler mux that responds with a `Validate` response.
+func HandleValidateSuccessfully(t *testing.T, output string) {
+ th.Mux.HandleFunc("/validate", 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, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, output)
+ })
+}
diff --git a/openstack/orchestration/v1/stacktemplates/requests.go b/openstack/orchestration/v1/stacktemplates/requests.go
new file mode 100644
index 0000000..5f8aba9
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/requests.go
@@ -0,0 +1,64 @@
+package stacktemplates
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// Get retreives data for the given stack template.
+func Get(c *gophercloud.ServiceClient, stackName, stackID string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(c, stackName, stackID), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// ValidateOptsBuilder describes struct types that can be accepted by the Validate call.
+// The ValidateOpts struct in this package does.
+type ValidateOptsBuilder interface {
+ ToStackTemplateValidateMap() (map[string]interface{}, error)
+}
+
+// ValidateOpts specifies the template validation parameters.
+type ValidateOpts struct {
+ Template map[string]interface{}
+ TemplateURL string
+}
+
+// ToStackTemplateValidateMap assembles a request body based on the contents of a ValidateOpts.
+func (opts ValidateOpts) ToStackTemplateValidateMap() (map[string]interface{}, error) {
+ vo := make(map[string]interface{})
+ if opts.Template != nil {
+ vo["template"] = opts.Template
+ return vo, nil
+ }
+ if opts.TemplateURL != "" {
+ vo["template_url"] = opts.TemplateURL
+ return vo, nil
+ }
+ return vo, fmt.Errorf("One of Template or TemplateURL is required.")
+}
+
+// Validate validates the given stack template.
+func Validate(c *gophercloud.ServiceClient, opts ValidateOptsBuilder) ValidateResult {
+ var res ValidateResult
+
+ reqBody, err := opts.ToStackTemplateValidateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", validateURL(c), perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ ReqBody: reqBody,
+ Results: &res.Body,
+ OkCodes: []int{200},
+ })
+ return res
+}
diff --git a/openstack/orchestration/v1/stacktemplates/requests_test.go b/openstack/orchestration/v1/stacktemplates/requests_test.go
new file mode 100644
index 0000000..d31c4ac
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/requests_test.go
@@ -0,0 +1,57 @@
+package stacktemplates
+
+import (
+ "testing"
+
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t, GetOutput)
+
+ actual, err := Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := GetExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestValidateTemplate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleValidateSuccessfully(t, ValidateOutput)
+
+ opts := ValidateOpts{
+ Template: map[string]interface{}{
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": map[string]interface{}{
+ "flavor": map[string]interface{}{
+ "default": "m1.tiny",
+ "type": "string",
+ },
+ },
+ "resources": map[string]interface{}{
+ "hello_world": map[string]interface{}{
+ "type": "OS::Nova::Server",
+ "properties": map[string]interface{}{
+ "key_name": "heat_key",
+ "flavor": map[string]interface{}{
+ "get_param": "flavor",
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n",
+ },
+ },
+ },
+ },
+ }
+ actual, err := Validate(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := ValidateExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/openstack/orchestration/v1/stacktemplates/results.go b/openstack/orchestration/v1/stacktemplates/results.go
new file mode 100644
index 0000000..ac2f24b
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/results.go
@@ -0,0 +1,60 @@
+package stacktemplates
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+)
+
+// Template represents a stack template.
+type Template struct {
+ Description string `mapstructure:"description"`
+ HeatTemplateVersion string `mapstructure:"heat_template_version"`
+ Parameters map[string]interface{} `mapstructure:"parameters"`
+ Resources map[string]interface{} `mapstructure:"resources"`
+}
+
+// GetResult represents the result of a Get operation.
+type GetResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to a Template object and is called after a
+// Get operation.
+func (r GetResult) Extract() (*Template, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res Template
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ return &res, nil
+}
+
+// ValidatedTemplate represents the parsed object returned from a Validate request.
+type ValidatedTemplate struct {
+ Description string
+ Parameters map[string]interface{}
+}
+
+// ValidateResult represents the result of a Validate operation.
+type ValidateResult struct {
+ gophercloud.Result
+}
+
+// Extract returns a pointer to a ValidatedTemplate object and is called after a
+// Validate operation.
+func (r ValidateResult) Extract() (*ValidatedTemplate, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res ValidatedTemplate
+ if err := mapstructure.Decode(r.Body, &res); err != nil {
+ return nil, err
+ }
+
+ return &res, nil
+}
diff --git a/openstack/orchestration/v1/stacktemplates/urls.go b/openstack/orchestration/v1/stacktemplates/urls.go
new file mode 100644
index 0000000..c30b7ca
--- /dev/null
+++ b/openstack/orchestration/v1/stacktemplates/urls.go
@@ -0,0 +1,11 @@
+package stacktemplates
+
+import "github.com/rackspace/gophercloud"
+
+func getURL(c *gophercloud.ServiceClient, stackName, stackID string) string {
+ return c.ServiceURL("stacks", stackName, stackID, "template")
+}
+
+func validateURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("validate")
+}
diff --git a/params.go b/params.go
index 948783b..4d0f1e6 100644
--- a/params.go
+++ b/params.go
@@ -144,6 +144,17 @@
params.Add(tags[0], strconv.FormatInt(v.Int(), 10))
case reflect.Bool:
params.Add(tags[0], strconv.FormatBool(v.Bool()))
+ case reflect.Slice:
+ switch v.Type().Elem() {
+ case reflect.TypeOf(0):
+ for i := 0; i < v.Len(); i++ {
+ params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10))
+ }
+ default:
+ for i := 0; i < v.Len(); i++ {
+ params.Add(tags[0], v.Index(i).String())
+ }
+ }
}
} else {
// Otherwise, the field is not set.
diff --git a/params_test.go b/params_test.go
index 4a2c9fe..2f40eec 100644
--- a/params_test.go
+++ b/params_test.go
@@ -34,16 +34,23 @@
}
func TestBuildQueryString(t *testing.T) {
+ type testVar string
opts := struct {
- J int `q:"j"`
- R string `q:"r,required"`
- C bool `q:"c"`
+ J int `q:"j"`
+ R string `q:"r,required"`
+ C bool `q:"c"`
+ S []string `q:"s"`
+ TS []testVar `q:"ts"`
+ TI []int `q:"ti"`
}{
- J: 2,
- R: "red",
- C: true,
+ J: 2,
+ R: "red",
+ C: true,
+ S: []string{"one", "two", "three"},
+ TS: []testVar{"a", "b"},
+ TI: []int{1, 2},
}
- expected := &url.URL{RawQuery: "c=true&j=2&r=red"}
+ expected := &url.URL{RawQuery: "c=true&j=2&r=red&s=one&s=two&s=three&ti=1&ti=2&ts=a&ts=b"}
actual, err := BuildQueryString(&opts)
if err != nil {
t.Errorf("Error building query string: %v", err)
@@ -51,9 +58,12 @@
th.CheckDeepEquals(t, expected, actual)
opts = struct {
- J int `q:"j"`
- R string `q:"r,required"`
- C bool `q:"c"`
+ J int `q:"j"`
+ R string `q:"r,required"`
+ C bool `q:"c"`
+ S []string `q:"s"`
+ TS []testVar `q:"ts"`
+ TI []int `q:"ti"`
}{
J: 2,
C: true,
diff --git a/rackspace/client.go b/rackspace/client.go
index 45199a4..439d846 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -187,3 +187,13 @@
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
+
+// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service.
+func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("orchestration")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
+}
diff --git a/rackspace/orchestration/v1/buildinfo/delegate.go b/rackspace/orchestration/v1/buildinfo/delegate.go
new file mode 100644
index 0000000..c834e5c
--- /dev/null
+++ b/rackspace/orchestration/v1/buildinfo/delegate.go
@@ -0,0 +1,11 @@
+package buildinfo
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo"
+)
+
+// Get retreives build info data for the Heat deployment.
+func Get(c *gophercloud.ServiceClient) os.GetResult {
+ return os.Get(c)
+}
diff --git a/rackspace/orchestration/v1/buildinfo/delegate_test.go b/rackspace/orchestration/v1/buildinfo/delegate_test.go
new file mode 100644
index 0000000..b25a690
--- /dev/null
+++ b/rackspace/orchestration/v1/buildinfo/delegate_test.go
@@ -0,0 +1,21 @@
+package buildinfo
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/buildinfo"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleGetSuccessfully(t, os.GetOutput)
+
+ actual, err := Get(fake.ServiceClient()).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.GetExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/orchestration/v1/buildinfo/doc.go b/rackspace/orchestration/v1/buildinfo/doc.go
new file mode 100644
index 0000000..183e8df
--- /dev/null
+++ b/rackspace/orchestration/v1/buildinfo/doc.go
@@ -0,0 +1,2 @@
+// Package buildinfo provides build information about heat deployments.
+package buildinfo
diff --git a/rackspace/orchestration/v1/stackevents/delegate.go b/rackspace/orchestration/v1/stackevents/delegate.go
new file mode 100644
index 0000000..08675de
--- /dev/null
+++ b/rackspace/orchestration/v1/stackevents/delegate.go
@@ -0,0 +1,27 @@
+package stackevents
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Find retreives stack events for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) os.FindResult {
+ return os.Find(c, stackName)
+}
+
+// List makes a request against the API to list resources for the given stack.
+func List(c *gophercloud.ServiceClient, stackName, stackID string, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(c, stackName, stackID, opts)
+}
+
+// ListResourceEvents makes a request against the API to list resources for the given stack.
+func ListResourceEvents(c *gophercloud.ServiceClient, stackName, stackID, resourceName string, opts os.ListResourceEventsOptsBuilder) pagination.Pager {
+ return os.ListResourceEvents(c, stackName, stackID, resourceName, opts)
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName, eventID string) os.GetResult {
+ return os.Get(c, stackName, stackID, resourceName, eventID)
+}
diff --git a/rackspace/orchestration/v1/stackevents/delegate_test.go b/rackspace/orchestration/v1/stackevents/delegate_test.go
new file mode 100644
index 0000000..e1c0bc8
--- /dev/null
+++ b/rackspace/orchestration/v1/stackevents/delegate_test.go
@@ -0,0 +1,72 @@
+package stackevents
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackevents"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindEvents(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleFindSuccessfully(t, os.FindOutput)
+
+ actual, err := Find(fake.ServiceClient(), "postman_stack").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.FindExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleListSuccessfully(t, os.ListOutput)
+
+ count := 0
+ err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractEvents(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, os.ListExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
+
+func TestListResourceEvents(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleListResourceEventsSuccessfully(t, os.ListResourceEventsOutput)
+
+ count := 0
+ err := ListResourceEvents(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractEvents(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, os.ListResourceEventsExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
+
+func TestGetEvent(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleGetSuccessfully(t, os.GetOutput)
+
+ actual, err := Get(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", "my_resource", "93940999-7d40-44ae-8de4-19624e7b8d18").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.GetExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/orchestration/v1/stackevents/doc.go b/rackspace/orchestration/v1/stackevents/doc.go
new file mode 100644
index 0000000..dfd6ef6
--- /dev/null
+++ b/rackspace/orchestration/v1/stackevents/doc.go
@@ -0,0 +1,3 @@
+// Package stackevents provides operations for finding, listing, and retrieving
+// stack events.
+package stackevents
diff --git a/rackspace/orchestration/v1/stackresources/delegate.go b/rackspace/orchestration/v1/stackresources/delegate.go
new file mode 100644
index 0000000..cb7be28
--- /dev/null
+++ b/rackspace/orchestration/v1/stackresources/delegate.go
@@ -0,0 +1,42 @@
+package stackresources
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Find retreives stack resources for the given stack name.
+func Find(c *gophercloud.ServiceClient, stackName string) os.FindResult {
+ return os.Find(c, stackName)
+}
+
+// List makes a request against the API to list resources for the given stack.
+func List(c *gophercloud.ServiceClient, stackName, stackID string, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(c, stackName, stackID, opts)
+}
+
+// Get retreives data for the given stack resource.
+func Get(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) os.GetResult {
+ return os.Get(c, stackName, stackID, resourceName)
+}
+
+// Metadata retreives the metadata for the given stack resource.
+func Metadata(c *gophercloud.ServiceClient, stackName, stackID, resourceName string) os.MetadataResult {
+ return os.Metadata(c, stackName, stackID, resourceName)
+}
+
+// ListTypes makes a request against the API to list resource types.
+func ListTypes(c *gophercloud.ServiceClient) pagination.Pager {
+ return os.ListTypes(c)
+}
+
+// Schema retreives the schema for the given resource type.
+func Schema(c *gophercloud.ServiceClient, resourceType string) os.SchemaResult {
+ return os.Schema(c, resourceType)
+}
+
+// Template retreives the template representation for the given resource type.
+func Template(c *gophercloud.ServiceClient, resourceType string) os.TemplateResult {
+ return os.Template(c, resourceType)
+}
diff --git a/rackspace/orchestration/v1/stackresources/delegate_test.go b/rackspace/orchestration/v1/stackresources/delegate_test.go
new file mode 100644
index 0000000..18e9614
--- /dev/null
+++ b/rackspace/orchestration/v1/stackresources/delegate_test.go
@@ -0,0 +1,108 @@
+package stackresources
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stackresources"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestFindResources(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleFindSuccessfully(t, os.FindOutput)
+
+ actual, err := Find(fake.ServiceClient(), "hello_world").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.FindExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResources(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleListSuccessfully(t, os.ListOutput)
+
+ count := 0
+ err := List(fake.ServiceClient(), "hello_world", "49181cd6-169a-4130-9455-31185bbfc5bf", nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractResources(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, os.ListExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
+
+func TestGetResource(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleGetSuccessfully(t, os.GetOutput)
+
+ actual, err := Get(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.GetExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestResourceMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleMetadataSuccessfully(t, os.MetadataOutput)
+
+ actual, err := Metadata(fake.ServiceClient(), "teststack", "0b1771bd-9336-4f2b-ae86-a80f971faf1e", "wordpress_instance").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.MetadataExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListResourceTypes(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleListTypesSuccessfully(t, os.ListTypesOutput)
+
+ count := 0
+ err := ListTypes(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractResourceTypes(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, os.ListTypesExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, 1, count)
+}
+
+func TestGetResourceSchema(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleGetSchemaSuccessfully(t, os.GetSchemaOutput)
+
+ actual, err := Schema(fake.ServiceClient(), "OS::Heat::AResourceName").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.GetSchemaExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestGetResourceTemplate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleGetTemplateSuccessfully(t, os.GetTemplateOutput)
+
+ actual, err := Template(fake.ServiceClient(), "OS::Heat::AResourceName").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.GetTemplateExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/orchestration/v1/stackresources/doc.go b/rackspace/orchestration/v1/stackresources/doc.go
new file mode 100644
index 0000000..e4f8b08
--- /dev/null
+++ b/rackspace/orchestration/v1/stackresources/doc.go
@@ -0,0 +1,5 @@
+// Package stackresources provides operations for working with stack resources.
+// A resource is a template artifact that represents some component of your
+// desired architecture (a Cloud Server, a group of scaled Cloud Servers, a load
+// balancer, some configuration management system, and so forth).
+package stackresources
diff --git a/rackspace/orchestration/v1/stacks/delegate.go b/rackspace/orchestration/v1/stacks/delegate.go
new file mode 100644
index 0000000..f7e387f
--- /dev/null
+++ b/rackspace/orchestration/v1/stacks/delegate.go
@@ -0,0 +1,49 @@
+package stacks
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Create accepts an os.CreateOpts struct and creates a new stack using the values
+// provided.
+func Create(c *gophercloud.ServiceClient, opts os.CreateOptsBuilder) os.CreateResult {
+ return os.Create(c, opts)
+}
+
+// Adopt accepts an os.AdoptOpts struct and creates a new stack from existing stack
+// resources using the values provided.
+func Adopt(c *gophercloud.ServiceClient, opts os.AdoptOptsBuilder) os.AdoptResult {
+ return os.Adopt(c, opts)
+}
+
+// List accepts an os.ListOpts struct and lists stacks based on the options provided.
+func List(c *gophercloud.ServiceClient, opts os.ListOptsBuilder) pagination.Pager {
+ return os.List(c, opts)
+}
+
+// Get retreives a stack based on the stack name and stack ID.
+func Get(c *gophercloud.ServiceClient, stackName, stackID string) os.GetResult {
+ return os.Get(c, stackName, stackID)
+}
+
+// Update accepts an os.UpdateOpts struct and updates a stack based on the options provided.
+func Update(c *gophercloud.ServiceClient, stackName, stackID string, opts os.UpdateOptsBuilder) os.UpdateResult {
+ return os.Update(c, stackName, stackID, opts)
+}
+
+// Delete deletes a stack based on the stack name and stack ID provided.
+func Delete(c *gophercloud.ServiceClient, stackName, stackID string) os.DeleteResult {
+ return os.Delete(c, stackName, stackID)
+}
+
+// Preview provides a preview of a stack based on the options provided.
+func Preview(c *gophercloud.ServiceClient, opts os.PreviewOptsBuilder) os.PreviewResult {
+ return os.Preview(c, opts)
+}
+
+// Abandon abandons a stack, keeping the resources available.
+func Abandon(c *gophercloud.ServiceClient, stackName, stackID string) os.AbandonResult {
+ return os.Abandon(c, stackName, stackID)
+}
diff --git a/rackspace/orchestration/v1/stacks/delegate_test.go b/rackspace/orchestration/v1/stacks/delegate_test.go
new file mode 100644
index 0000000..a1fb393
--- /dev/null
+++ b/rackspace/orchestration/v1/stacks/delegate_test.go
@@ -0,0 +1,461 @@
+package stacks
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestCreateStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleCreateSuccessfully(t, CreateOutput)
+
+ createOpts := os.CreateOpts{
+ Name: "stackcreated",
+ Timeout: 60,
+ Template: `{
+ "outputs": {
+ "db_host": {
+ "value": {
+ "get_attr": [
+ "db",
+ "hostname"
+ ]
+ }
+ }
+ },
+ "heat_template_version": "2014-10-16",
+ "description": "HEAT template for creating a Cloud Database.\n",
+ "parameters": {
+ "db_name": {
+ "default": "wordpress",
+ "type": "string",
+ "description": "the name for the database",
+ "constraints": [
+ {
+ "length": {
+ "max": 64,
+ "min": 1
+ },
+ "description": "must be between 1 and 64 characters"
+ },
+ {
+ "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+ "description": "must begin with a letter and contain only alphanumeric characters."
+ }
+ ]
+ },
+ "db_instance_name": {
+ "default": "Cloud_DB",
+ "type": "string",
+ "description": "the database instance name"
+ },
+ "db_username": {
+ "default": "admin",
+ "hidden": true,
+ "type": "string",
+ "description": "database admin account username",
+ "constraints": [
+ {
+ "length": {
+ "max": 16,
+ "min": 1
+ },
+ "description": "must be between 1 and 16 characters"
+ },
+ {
+ "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+ "description": "must begin with a letter and contain only alphanumeric characters."
+ }
+ ]
+ },
+ "db_volume_size": {
+ "default": 30,
+ "type": "number",
+ "description": "database volume size (in GB)",
+ "constraints": [
+ {
+ "range": {
+ "max": 1024,
+ "min": 1
+ },
+ "description": "must be between 1 and 1024 GB"
+ }
+ ]
+ },
+ "db_flavor": {
+ "default": "1GB Instance",
+ "type": "string",
+ "description": "database instance size",
+ "constraints": [
+ {
+ "description": "must be a valid cloud database flavor",
+ "allowed_values": [
+ "1GB Instance",
+ "2GB Instance",
+ "4GB Instance",
+ "8GB Instance",
+ "16GB Instance"
+ ]
+ }
+ ]
+ },
+ "db_password": {
+ "default": "admin",
+ "hidden": true,
+ "type": "string",
+ "description": "database admin account password",
+ "constraints": [
+ {
+ "length": {
+ "max": 41,
+ "min": 1
+ },
+ "description": "must be between 1 and 14 characters"
+ },
+ {
+ "allowed_pattern": "[a-zA-Z0-9]*",
+ "description": "must contain only alphanumeric characters."
+ }
+ ]
+ }
+ },
+ "resources": {
+ "db": {
+ "type": "OS::Trove::Instance",
+ "properties": {
+ "flavor": {
+ "get_param": "db_flavor"
+ },
+ "size": {
+ "get_param": "db_volume_size"
+ },
+ "users": [
+ {
+ "password": {
+ "get_param": "db_password"
+ },
+ "name": {
+ "get_param": "db_username"
+ },
+ "databases": [
+ {
+ "get_param": "db_name"
+ }
+ ]
+ }
+ ],
+ "name": {
+ "get_param": "db_instance_name"
+ },
+ "databases": [
+ {
+ "name": {
+ "get_param": "db_name"
+ }
+ }
+ ]
+ }
+ }
+ }
+ }`,
+ DisableRollback: os.Disable,
+ }
+ actual, err := Create(fake.ServiceClient(), createOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := CreateExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestAdoptStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleCreateSuccessfully(t, CreateOutput)
+
+ adoptOpts := os.AdoptOpts{
+ AdoptStackData: `{\"environment\":{\"parameters\":{}}, \"status\":\"COMPLETE\",\"name\": \"trovestack\",\n \"template\": {\n \"outputs\": {\n \"db_host\": {\n \"value\": {\n \"get_attr\": [\n \"db\",\n \"hostname\"\n ]\n }\n }\n },\n \"heat_template_version\": \"2014-10-16\",\n \"description\": \"HEAT template for creating a Cloud Database.\\n\",\n \"parameters\": {\n \"db_instance_name\": {\n \"default\": \"Cloud_DB\",\n \"type\": \"string\",\n \"description\": \"the database instance name\"\n },\n \"db_flavor\": {\n \"default\": \"1GB Instance\",\n \"type\": \"string\",\n \"description\": \"database instance size\",\n \"constraints\": [\n {\n \"description\": \"must be a valid cloud database flavor\",\n \"allowed_values\": [\n \"1GB Instance\",\n \"2GB Instance\",\n \"4GB Instance\",\n \"8GB Instance\",\n \"16GB Instance\"\n ]\n }\n ]\n },\n \"db_password\": {\n \"default\": \"admin\",\n \"hidden\": true,\n \"type\": \"string\",\n \"description\": \"database admin account password\",\n \"constraints\": [\n {\n \"length\": {\n \"max\": 41,\n \"min\": 1\n },\n \"description\": \"must be between 1 and 14 characters\"\n },\n {\n \"allowed_pattern\": \"[a-zA-Z0-9]*\",\n \"description\": \"must contain only alphanumeric characters.\"\n }\n ]\n },\n \"db_name\": {\n \"default\": \"wordpress\",\n \"type\": \"string\",\n \"description\": \"the name for the database\",\n \"constraints\": [\n {\n \"length\": {\n \"max\": 64,\n \"min\": 1\n },\n \"description\": \"must be between 1 and 64 characters\"\n },\n {\n \"allowed_pattern\": \"[a-zA-Z][a-zA-Z0-9]*\",\n \"description\": \"must begin with a letter and contain only alphanumeric characters.\"\n }\n ]\n },\n \"db_username\": {\n \"default\": \"admin\",\n \"hidden\": true,\n \"type\": \"string\",\n \"description\": \"database admin account username\",\n \"constraints\": [\n {\n \"length\": {\n \"max\": 16,\n \"min\": 1\n },\n \"description\": \"must be between 1 and 16 characters\"\n },\n {\n \"allowed_pattern\": \"[a-zA-Z][a-zA-Z0-9]*\",\n \"description\": \"must begin with a letter and contain only alphanumeric characters.\"\n }\n ]\n },\n \"db_volume_size\": {\n \"default\": 30,\n \"type\": \"number\",\n \"description\": \"database volume size (in GB)\",\n \"constraints\": [\n {\n \"range\": {\n \"max\": 1024,\n \"min\": 1\n },\n \"description\": \"must be between 1 and 1024 GB\"\n }\n ]\n }\n },\n \"resources\": {\n \"db\": {\n \"type\": \"OS::Trove::Instance\",\n \"properties\": {\n \"flavor\": {\n \"get_param\": \"db_flavor\"\n },\n \"databases\": [\n {\n \"name\": {\n \"get_param\": \"db_name\"\n }\n }\n ],\n \"users\": [\n {\n \"password\": {\n \"get_param\": \"db_password\"\n },\n \"name\": {\n \"get_param\": \"db_username\"\n },\n \"databases\": [\n {\n \"get_param\": \"db_name\"\n }\n ]\n }\n ],\n \"name\": {\n \"get_param\": \"db_instance_name\"\n },\n \"size\": {\n \"get_param\": \"db_volume_size\"\n }\n }\n }\n }\n },\n \"action\": \"CREATE\",\n \"id\": \"exxxxd-7xx5-4xxb-bxx2-cxxxxxx5\",\n \"resources\": {\n \"db\": {\n \"status\": \"COMPLETE\",\n \"name\": \"db\",\n \"resource_data\": {},\n \"resource_id\": \"exxxx2-9xx0-4xxxb-bxx2-dxxxxxx4\",\n \"action\": \"CREATE\",\n \"type\": \"OS::Trove::Instance\",\n \"metadata\": {}\n }\n }\n},`,
+ Name: "stackadopted",
+ Timeout: 60,
+ Template: `{
+ "outputs": {
+ "db_host": {
+ "value": {
+ "get_attr": [
+ "db",
+ "hostname"
+ ]
+ }
+ }
+ },
+ "heat_template_version": "2014-10-16",
+ "description": "HEAT template for creating a Cloud Database.\n",
+ "parameters": {
+ "db_name": {
+ "default": "wordpress",
+ "type": "string",
+ "description": "the name for the database",
+ "constraints": [
+ {
+ "length": {
+ "max": 64,
+ "min": 1
+ },
+ "description": "must be between 1 and 64 characters"
+ },
+ {
+ "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+ "description": "must begin with a letter and contain only alphanumeric characters."
+ }
+ ]
+ },
+ "db_instance_name": {
+ "default": "Cloud_DB",
+ "type": "string",
+ "description": "the database instance name"
+ },
+ "db_username": {
+ "default": "admin",
+ "hidden": true,
+ "type": "string",
+ "description": "database admin account username",
+ "constraints": [
+ {
+ "length": {
+ "max": 16,
+ "min": 1
+ },
+ "description": "must be between 1 and 16 characters"
+ },
+ {
+ "allowed_pattern": "[a-zA-Z][a-zA-Z0-9]*",
+ "description": "must begin with a letter and contain only alphanumeric characters."
+ }
+ ]
+ },
+ "db_volume_size": {
+ "default": 30,
+ "type": "number",
+ "description": "database volume size (in GB)",
+ "constraints": [
+ {
+ "range": {
+ "max": 1024,
+ "min": 1
+ },
+ "description": "must be between 1 and 1024 GB"
+ }
+ ]
+ },
+ "db_flavor": {
+ "default": "1GB Instance",
+ "type": "string",
+ "description": "database instance size",
+ "constraints": [
+ {
+ "description": "must be a valid cloud database flavor",
+ "allowed_values": [
+ "1GB Instance",
+ "2GB Instance",
+ "4GB Instance",
+ "8GB Instance",
+ "16GB Instance"
+ ]
+ }
+ ]
+ },
+ "db_password": {
+ "default": "admin",
+ "hidden": true,
+ "type": "string",
+ "description": "database admin account password",
+ "constraints": [
+ {
+ "length": {
+ "max": 41,
+ "min": 1
+ },
+ "description": "must be between 1 and 14 characters"
+ },
+ {
+ "allowed_pattern": "[a-zA-Z0-9]*",
+ "description": "must contain only alphanumeric characters."
+ }
+ ]
+ }
+ },
+ "resources": {
+ "db": {
+ "type": "OS::Trove::Instance",
+ "properties": {
+ "flavor": {
+ "get_param": "db_flavor"
+ },
+ "size": {
+ "get_param": "db_volume_size"
+ },
+ "users": [
+ {
+ "password": {
+ "get_param": "db_password"
+ },
+ "name": {
+ "get_param": "db_username"
+ },
+ "databases": [
+ {
+ "get_param": "db_name"
+ }
+ ]
+ }
+ ],
+ "name": {
+ "get_param": "db_instance_name"
+ },
+ "databases": [
+ {
+ "name": {
+ "get_param": "db_name"
+ }
+ }
+ ]
+ }
+ }
+ }
+ }`,
+ DisableRollback: os.Disable,
+ }
+ actual, err := Adopt(fake.ServiceClient(), adoptOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := CreateExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestListStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleListSuccessfully(t, os.FullListOutput)
+
+ count := 0
+ err := List(fake.ServiceClient(), nil).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := os.ExtractStacks(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, os.ListExpected, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
+
+func TestUpdateStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleUpdateSuccessfully(t)
+
+ updateOpts := os.UpdateOpts{
+ Template: `
+ {
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {
+ "flavor": {
+ "default": "m1.tiny",
+ "type": "string"
+ }
+ },
+ "resources": {
+ "hello_world": {
+ "type":"OS::Nova::Server",
+ "properties": {
+ "key_name": "heat_key",
+ "flavor": {
+ "get_param": "flavor"
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+ }`,
+ }
+ err := Update(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada", updateOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestDeleteStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleDeleteSuccessfully(t)
+
+ err := Delete(fake.ServiceClient(), "gophercloud-test-stack-2", "db6977b2-27aa-4775-9ae7-6213212d4ada").ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestPreviewStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandlePreviewSuccessfully(t, os.GetOutput)
+
+ previewOpts := os.PreviewOpts{
+ Name: "stackcreated",
+ Timeout: 60,
+ Template: `
+ {
+ "stack_name": "postman_stack",
+ "template": {
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": {
+ "flavor": {
+ "default": "m1.tiny",
+ "type": "string"
+ }
+ },
+ "resources": {
+ "hello_world": {
+ "type":"OS::Nova::Server",
+ "properties": {
+ "key_name": "heat_key",
+ "flavor": {
+ "get_param": "flavor"
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n"
+ }
+ }
+ }
+ }
+ }`,
+ DisableRollback: os.Disable,
+ }
+ actual, err := Preview(fake.ServiceClient(), previewOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.PreviewExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+/*
+func TestAbandonStack(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleAbandonSuccessfully(t)
+
+ //actual, err := Abandon(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract()
+ //th.AssertNoErr(t, err)
+ res := Abandon(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87") //.Extract()
+ th.AssertNoErr(t, res.Err)
+ t.Logf("actual: %+v", res)
+
+ //expected := os.AbandonExpected
+ //th.AssertDeepEquals(t, expected, actual)
+}
+*/
diff --git a/rackspace/orchestration/v1/stacks/doc.go b/rackspace/orchestration/v1/stacks/doc.go
new file mode 100644
index 0000000..19231b5
--- /dev/null
+++ b/rackspace/orchestration/v1/stacks/doc.go
@@ -0,0 +1,8 @@
+// Package stacks provides operation for working with Heat stacks. A stack is a
+// group of resources (servers, load balancers, databases, and so forth)
+// combined to fulfill a useful purpose. Based on a template, Heat orchestration
+// engine creates an instantiated set of resources (a stack) to run the
+// application framework or component specified (in the template). A stack is a
+// running instance of a template. The result of creating a stack is a deployment
+// of the application framework or component.
+package stacks
diff --git a/rackspace/orchestration/v1/stacks/fixtures.go b/rackspace/orchestration/v1/stacks/fixtures.go
new file mode 100644
index 0000000..c9afeb1
--- /dev/null
+++ b/rackspace/orchestration/v1/stacks/fixtures.go
@@ -0,0 +1,32 @@
+package stacks
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacks"
+)
+
+// CreateExpected represents the expected object from a Create request.
+var CreateExpected = &os.CreatedStack{
+ ID: "b663e18a-4767-4cdf-9db5-9c8cc13cc38a",
+ Links: []gophercloud.Link{
+ gophercloud.Link{
+ Href: "https://ord.orchestration.api.rackspacecloud.com/v1/864477/stacks/stackcreated/b663e18a-4767-4cdf-9db5-9c8cc13cc38a",
+ Rel: "self",
+ },
+ },
+}
+
+// CreateOutput represents the response body from a Create request.
+const CreateOutput = `
+{
+ "stack": {
+ "id": "b663e18a-4767-4cdf-9db5-9c8cc13cc38a",
+ "links": [
+ {
+ "href": "https://ord.orchestration.api.rackspacecloud.com/v1/864477/stacks/stackcreated/b663e18a-4767-4cdf-9db5-9c8cc13cc38a",
+ "rel": "self"
+ }
+ ]
+ }
+}
+`
diff --git a/rackspace/orchestration/v1/stacktemplates/delegate.go b/rackspace/orchestration/v1/stacktemplates/delegate.go
new file mode 100644
index 0000000..3b5d46e
--- /dev/null
+++ b/rackspace/orchestration/v1/stacktemplates/delegate.go
@@ -0,0 +1,16 @@
+package stacktemplates
+
+import (
+ "github.com/rackspace/gophercloud"
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates"
+)
+
+// Get retreives data for the given stack template.
+func Get(c *gophercloud.ServiceClient, stackName, stackID string) os.GetResult {
+ return os.Get(c, stackName, stackID)
+}
+
+// Validate validates the given stack template.
+func Validate(c *gophercloud.ServiceClient, opts os.ValidateOptsBuilder) os.ValidateResult {
+ return os.Validate(c, opts)
+}
diff --git a/rackspace/orchestration/v1/stacktemplates/delegate_test.go b/rackspace/orchestration/v1/stacktemplates/delegate_test.go
new file mode 100644
index 0000000..d4006c4
--- /dev/null
+++ b/rackspace/orchestration/v1/stacktemplates/delegate_test.go
@@ -0,0 +1,58 @@
+package stacktemplates
+
+import (
+ "testing"
+
+ os "github.com/rackspace/gophercloud/openstack/orchestration/v1/stacktemplates"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestGetTemplate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleGetSuccessfully(t, os.GetOutput)
+
+ actual, err := Get(fake.ServiceClient(), "postman_stack", "16ef0584-4458-41eb-87c8-0dc8d5f66c87").Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.GetExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
+
+func TestValidateTemplate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ os.HandleValidateSuccessfully(t, os.ValidateOutput)
+
+ opts := os.ValidateOpts{
+ Template: map[string]interface{}{
+ "heat_template_version": "2013-05-23",
+ "description": "Simple template to test heat commands",
+ "parameters": map[string]interface{}{
+ "flavor": map[string]interface{}{
+ "default": "m1.tiny",
+ "type": "string",
+ },
+ },
+ "resources": map[string]interface{}{
+ "hello_world": map[string]interface{}{
+ "type": "OS::Nova::Server",
+ "properties": map[string]interface{}{
+ "key_name": "heat_key",
+ "flavor": map[string]interface{}{
+ "get_param": "flavor",
+ },
+ "image": "ad091b52-742f-469e-8f3c-fd81cadf0743",
+ "user_data": "#!/bin/bash -xv\necho \"hello world\" > /root/hello-world.txt\n",
+ },
+ },
+ },
+ },
+ }
+ actual, err := Validate(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ expected := os.ValidateExpected
+ th.AssertDeepEquals(t, expected, actual)
+}
diff --git a/rackspace/orchestration/v1/stacktemplates/doc.go b/rackspace/orchestration/v1/stacktemplates/doc.go
new file mode 100644
index 0000000..5af0bd6
--- /dev/null
+++ b/rackspace/orchestration/v1/stacktemplates/doc.go
@@ -0,0 +1,8 @@
+// Package stacktemplates provides operations for working with Heat templates.
+// A Cloud Orchestration template is a portable file, written in a user-readable
+// language, that describes how a set of resources should be assembled and what
+// software should be installed in order to produce a working stack. The template
+// specifies what resources should be used, what attributes can be set, and other
+// parameters that are critical to the successful, repeatable automation of a
+// specific application stack.
+package stacktemplates