Add template and environment parsing to gophercloud

Openstack Heat expects the client to do some parsing client side,
specifically for nested templates and environments which refer
to local files. This patch adds a recursive parser for both the
template and environment files to gophercloud. The interfaces
are also changed to make use of the new parsing functionality.
diff --git a/openstack/orchestration/v1/stacks/environment.go b/openstack/orchestration/v1/stacks/environment.go
new file mode 100644
index 0000000..378eeaf
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/environment.go
@@ -0,0 +1,121 @@
+package stacks
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+)
+
+// an interface to represent stack environments
+type Environment struct {
+	TE
+}
+
+// allowed sections in a stack environment file
+var EnvironmentSections = map[string]bool{
+	"parameters":         true,
+	"parameter_defaults": true,
+	"resource_registry":  true,
+}
+
+func (e *Environment) Validate() error {
+	if e.Parsed == nil {
+		if err := e.Parse(); err != nil {
+			return err
+		}
+	}
+	for key, _ := range e.Parsed {
+		if _, ok := EnvironmentSections[key]; !ok {
+			return errors.New(fmt.Sprintf("Environment has wrong section: %s", key))
+		}
+	}
+	return nil
+}
+
+// Parse environment file to resolve the urls of the resources
+func GetRRFileContents(e *Environment, ignoreIf igFunc) error {
+	if e.Files == nil {
+		e.Files = make(map[string]string)
+	}
+	if e.fileMaps == nil {
+		e.fileMaps = make(map[string]string)
+	}
+	rr := e.Parsed["resource_registry"]
+	// search the resource registry for URLs
+	switch rr.(type) {
+	case map[string]interface{}, map[interface{}]interface{}:
+		rr_map, err := toStringKeys(rr)
+		if err != nil {
+			return err
+		}
+		var baseURL string
+		if val, ok := rr_map["base_url"]; ok {
+			baseURL = val.(string)
+		} else {
+			baseURL = e.baseURL
+		}
+		// use a fake template to fetch contents from URLs
+		tempTemplate := new(Template)
+		tempTemplate.baseURL = baseURL
+		tempTemplate.client = e.client
+
+		if err = GetFileContents(tempTemplate, rr, ignoreIf, false); err != nil {
+			return err
+		}
+		// check the `resources` section (if it exists) for more URLs
+		if val, ok := rr_map["resources"]; ok {
+			switch val.(type) {
+			case map[string]interface{}, map[interface{}]interface{}:
+				resources_map, err := toStringKeys(val)
+				if err != nil {
+					return err
+				}
+				for _, v := range resources_map {
+					switch v.(type) {
+					case map[string]interface{}, map[interface{}]interface{}:
+						resource_map, err := toStringKeys(v)
+						if err != nil {
+							return err
+						}
+						var resourceBaseURL string
+						// if base_url for the resource type is defined, use it
+						if val, ok := resource_map["base_url"]; ok {
+							resourceBaseURL = val.(string)
+						} else {
+							resourceBaseURL = baseURL
+						}
+						tempTemplate.baseURL = resourceBaseURL
+						if err := GetFileContents(tempTemplate, v, ignoreIf, false); err != nil {
+							return err
+						}
+					}
+
+				}
+
+			}
+		}
+		e.Files = tempTemplate.Files
+		return nil
+	default:
+		return nil
+	}
+}
+
+// function to choose keys whose values are other environment files
+func ignoreIfEnvironment(key string, value interface{}) bool {
+	// base_url and hooks refer to components which cannot have urls
+	if key == "base_url" || key == "hooks" {
+		return true
+	}
+	// if value is not string, it cannot be a URL
+	valueString, ok := value.(string)
+	if !ok {
+		return true
+	}
+	// if value contains `::`, it must be a reference to another resource type
+	// e.g. OS::Nova::Server : Rackspace::Cloud::Server
+	if strings.Contains(valueString, "::") {
+		return true
+	}
+	return false
+}
diff --git a/openstack/orchestration/v1/stacks/environment_test.go b/openstack/orchestration/v1/stacks/environment_test.go
new file mode 100644
index 0000000..7d5aaec
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/environment_test.go
@@ -0,0 +1,184 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestEnvironmentValidation(t *testing.T) {
+	environmentJSON := new(Environment)
+	environmentJSON.Bin = []byte(ValidJSONEnvironment)
+	err := environmentJSON.Validate()
+	th.AssertNoErr(t, err)
+
+	environmentYAML := new(Environment)
+	environmentYAML.Bin = []byte(ValidYAMLEnvironment)
+	err = environmentYAML.Validate()
+	th.AssertNoErr(t, err)
+
+	environmentInvalid := new(Environment)
+	environmentInvalid.Bin = []byte(InvalidEnvironment)
+	if err = environmentInvalid.Validate(); err == nil {
+		t.Error("environment validation did not catch invalid environment")
+	}
+}
+
+func TestEnvironmentParsing(t *testing.T) {
+	environmentJSON := new(Environment)
+	environmentJSON.Bin = []byte(ValidJSONEnvironment)
+	err := environmentJSON.Parse()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentJSON.Parsed)
+
+	environmentYAML := new(Environment)
+	environmentYAML.Bin = []byte(ValidJSONEnvironment)
+	err = environmentYAML.Parse()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, ValidJSONEnvironmentParsed, environmentYAML.Parsed)
+
+	environmentInvalid := new(Environment)
+	environmentInvalid.Bin = []byte("Keep Austin Weird")
+	err = environmentInvalid.Parse()
+	if err == nil {
+		t.Error("environment parsing did not catch invalid environment")
+	}
+}
+
+func TestIgnoreIfEnvironment(t *testing.T) {
+	var keyValueTests = []struct {
+		key   string
+		value interface{}
+		out   bool
+	}{
+		{"base_url", "afksdf", true},
+		{"not_type", "hooks", false},
+		{"get_file", "::", true},
+		{"hooks", "dfsdfsd", true},
+		{"type", "sdfubsduf.yaml", false},
+		{"type", "sdfsdufs.environment", false},
+		{"type", "sdfsdf.file", false},
+		{"type", map[string]string{"key": "value"}, true},
+	}
+	var result bool
+	for _, kv := range keyValueTests {
+		result = ignoreIfEnvironment(kv.key, kv.value)
+		if result != kv.out {
+			t.Errorf("key: %v, value: %v expected: %v, actual: %v", kv.key, kv.value, kv.out, result)
+		}
+	}
+}
+
+func TestGetRRFileContents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	environment_content := `
+heat_template_version: 2013-05-23
+
+description:
+  Heat WordPress template to support F18, using only Heat OpenStack-native
+  resource types, and without the requirement for heat-cfntools in the image.
+  WordPress is web software you can use to create a beautiful website or blog.
+  This template installs a single-instance WordPress deployment using a local
+  MySQL database to store the data.
+
+parameters:
+
+  key_name:
+    type: string
+    description : Name of a KeyPair to enable SSH access to the instance
+
+resources:
+  wordpress_instance:
+    type: OS::Nova::Server
+    properties:
+      image: { get_param: image_id }
+      flavor: { get_param: instance_type }
+      key_name: { get_param: key_name }`
+
+	db_content := `
+heat_template_version: 2014-10-16
+
+description:
+  Test template for Trove resource capabilities
+
+parameters:
+  db_pass:
+    type: string
+    hidden: true
+    description: Database access password
+    default: secrete
+
+resources:
+
+service_db:
+  type: OS::Trove::Instance
+  properties:
+    name: trove_test_db
+    datastore_type: mariadb
+    flavor: 1GB Instance
+    size: 10
+    databases:
+    - name: test_data
+    users:
+    - name: kitchen_sink
+      password: { get_param: db_pass }
+      databases: [ test_data ]`
+	baseurl, err := getBasePath()
+	th.AssertNoErr(t, err)
+
+	fakeEnvURL := strings.Join([]string{baseurl, "my_env.yaml"}, "/")
+	urlparsed, err := url.Parse(fakeEnvURL)
+	th.AssertNoErr(t, err)
+	// handler for my_env.yaml
+	th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		w.Header().Set("Content-Type", "application/jason")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, environment_content)
+	})
+
+	fakeDBURL := strings.Join([]string{baseurl, "my_db.yaml"}, "/")
+	urlparsed, err = url.Parse(fakeDBURL)
+	th.AssertNoErr(t, err)
+
+	// handler for my_db.yaml
+	th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		w.Header().Set("Content-Type", "application/jason")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, db_content)
+	})
+
+	client := fakeClient{BaseClient: getHTTPClient()}
+	env := new(Environment)
+	env.Bin = []byte(`{"resource_registry": {"My::WP::Server": "my_env.yaml", "resources": {"my_db_server": {"OS::DBInstance": "my_db.yaml"}}}}`)
+	env.client = client
+
+	err = env.Parse()
+	th.AssertNoErr(t, err)
+	err = GetRRFileContents(env, ignoreIfEnvironment)
+	th.AssertNoErr(t, err)
+	expected_env_files_content := "\nheat_template_version: 2013-05-23\n\ndescription:\n  Heat WordPress template to support F18, using only Heat OpenStack-native\n  resource types, and without the requirement for heat-cfntools in the image.\n  WordPress is web software you can use to create a beautiful website or blog.\n  This template installs a single-instance WordPress deployment using a local\n  MySQL database to store the data.\n\nparameters:\n\n  key_name:\n    type: string\n    description : Name of a KeyPair to enable SSH access to the instance\n\nresources:\n  wordpress_instance:\n    type: OS::Nova::Server\n    properties:\n      image: { get_param: image_id }\n      flavor: { get_param: instance_type }\n      key_name: { get_param: key_name }"
+	expected_db_files_content := "\nheat_template_version: 2014-10-16\n\ndescription:\n  Test template for Trove resource capabilities\n\nparameters:\n  db_pass:\n    type: string\n    hidden: true\n    description: Database access password\n    default: secrete\n\nresources:\n\nservice_db:\n  type: OS::Trove::Instance\n  properties:\n    name: trove_test_db\n    datastore_type: mariadb\n    flavor: 1GB Instance\n    size: 10\n    databases:\n    - name: test_data\n    users:\n    - name: kitchen_sink\n      password: { get_param: db_pass }\n      databases: [ test_data ]"
+
+	th.AssertEquals(t, expected_env_files_content, env.Files[fakeEnvURL])
+	th.AssertEquals(t, expected_db_files_content, env.Files[fakeDBURL])
+
+	env.FixFileRefs()
+	expected_parsed := map[string]interface{}{
+		"resource_registry": "2015-04-30",
+		"My::WP::Server":    fakeEnvURL,
+		"resources": map[string]interface{}{
+			"my_db_server": map[string]interface{}{
+				"OS::DBInstance": fakeDBURL,
+			},
+		},
+	}
+	env.Parse()
+	th.AssertDeepEquals(t, expected_parsed, env.Parsed)
+}
diff --git a/openstack/orchestration/v1/stacks/fixtures.go b/openstack/orchestration/v1/stacks/fixtures.go
index f884d51..0cb38c2 100644
--- a/openstack/orchestration/v1/stacks/fixtures.go
+++ b/openstack/orchestration/v1/stacks/fixtures.go
@@ -404,3 +404,193 @@
 		fmt.Fprintf(w, output)
 	})
 }
+
+const ValidJSONTemplate = `
+{
+  "heat_template_version": "2014-10-16",
+  "parameters": {
+    "flavor": {
+      "default": 4353,
+      "description": "Flavor for the server to be created",
+      "hidden": true,
+      "type": "string"
+    }
+  },
+  "resources": {
+    "test_server": {
+      "properties": {
+        "flavor": "2 GB General Purpose v1",
+        "image": "Debian 7 (Wheezy) (PVHVM)",
+        "name": "test-server"
+      },
+      "type": "OS::Nova::Server"
+    }
+  }
+}
+`
+
+var ValidJSONTemplateParsed = map[string]interface{}{
+	"heat_template_version": "2014-10-16",
+	"parameters": map[string]interface{}{
+		"flavor": map[string]interface{}{
+			"default":     4353,
+			"description": "Flavor for the server to be created",
+			"hidden":      true,
+			"type":        "string",
+		},
+	},
+	"resources": map[string]interface{}{
+		"test_server": map[string]interface{}{
+			"properties": map[string]interface{}{
+				"flavor": "2 GB General Purpose v1",
+				"image":  "Debian 7 (Wheezy) (PVHVM)",
+				"name":   "test-server",
+			},
+			"type": "OS::Nova::Server",
+		},
+	},
+}
+
+const ValidYAMLTemplate = `
+heat_template_version: 2014-10-16
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+`
+
+const InvalidTemplateNoVersion = `
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+`
+
+const ValidJSONEnvironment = `
+{
+  "parameters": {
+    "user_key": "userkey"
+  },
+  "resource_registry": {
+    "My::WP::Server": "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml",
+    "OS::Quantum*": "OS::Neutron*",
+    "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml",
+    "OS::Metering::Alarm": "OS::Ceilometer::Alarm",
+    "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml",
+    "resources": {
+      "my_db_server": {
+        "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml"
+      },
+      "my_server": {
+        "OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml",
+        "hooks": "pre-create"
+      },
+      "nested_stack": {
+        "nested_resource": {
+          "hooks": "pre-update"
+        },
+        "another_resource": {
+          "hooks": [
+            "pre-create",
+            "pre-update"
+          ]
+        }
+      }
+    }
+  }
+}
+`
+
+var ValidJSONEnvironmentParsed = map[string]interface{}{
+	"parameters": map[string]interface{}{
+		"user_key": "userkey",
+	},
+	"resource_registry": map[string]interface{}{
+		"My::WP::Server":         "file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml",
+		"OS::Quantum*":           "OS::Neutron*",
+		"AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml",
+		"OS::Metering::Alarm":    "OS::Ceilometer::Alarm",
+		"AWS::RDS::DBInstance":   "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml",
+		"resources": map[string]interface{}{
+			"my_db_server": map[string]interface{}{
+				"OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml",
+			},
+			"my_server": map[string]interface{}{
+				"OS::DBInstance": "file:///home/mine/all_my_cool_templates/db.yaml",
+				"hooks":          "pre-create",
+			},
+			"nested_stack": map[string]interface{}{
+				"nested_resource": map[string]interface{}{
+					"hooks": "pre-update",
+				},
+				"another_resource": map[string]interface{}{
+					"hooks": []interface{}{
+						"pre-create",
+						"pre-update",
+					},
+				},
+			},
+		},
+	},
+}
+
+const ValidYAMLEnvironment = `
+parameters:
+  user_key: userkey
+resource_registry:
+  My::WP::Server: file:///home/shardy/git/heat-templates/hot/F18/WordPress_Native.yaml
+  # allow older templates with Quantum in them.
+  "OS::Quantum*": "OS::Neutron*"
+  # Choose your implementation of AWS::CloudWatch::Alarm
+  "AWS::CloudWatch::Alarm": "file:///etc/heat/templates/AWS_CloudWatch_Alarm.yaml"
+  #"AWS::CloudWatch::Alarm": "OS::Heat::CWLiteAlarm"
+  "OS::Metering::Alarm": "OS::Ceilometer::Alarm"
+  "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.yaml"
+  resources:
+    my_db_server:
+      "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
+    my_server:
+      "OS::DBInstance": file:///home/mine/all_my_cool_templates/db.yaml
+      hooks: pre-create
+    nested_stack:
+      nested_resource:
+        hooks: pre-update
+      another_resource:
+        hooks: [pre-create, pre-update]
+`
+
+const InvalidEnvironment = `
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+parameter_defaults:
+  KeyName: heat_key
+`
diff --git a/openstack/orchestration/v1/stacks/requests.go b/openstack/orchestration/v1/stacks/requests.go
index bd8e59e..9859d4d 100644
--- a/openstack/orchestration/v1/stacks/requests.go
+++ b/openstack/orchestration/v1/stacks/requests.go
@@ -33,9 +33,16 @@
 type CreateOpts struct {
 	// (REQUIRED) The name of the stack. It must start with an alphabetic character.
 	Name string
+	// (REQUIRED) A structure that contains either the template file or url. Call the
+	// associated methods to extract the information relevant to send in a create request.
+	TemplateOpts *Template
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, TemplateURL will be ignored
 	// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
 	// This value is ignored if Template is supplied inline.
 	TemplateURL string
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, Template will be ignored
 	// (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:
@@ -51,8 +58,14 @@
 	// creation fails. Default is true, meaning all resources are not deleted when
 	// stack creation fails.
 	DisableRollback Rollback
+	// (OPTIONAL) A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment
+	// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
 	// (OPTIONAL) A stringified JSON environment for the stack.
 	Environment string
+	// (DEPRECATED): Files is automatically determined
+	// by parsing the template and environment passed as TemplateOpts and
+	// EnvironmentOpts respectively.
 	// (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"}`
@@ -73,25 +86,60 @@
 		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
+	Files := make(map[string]string)
+	if opts.TemplateOpts == nil {
+		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.")
+		}
 	} else {
-		return s, errors.New("Either Template or TemplateURL must be provided.")
+		if err := opts.TemplateOpts.Parse(); err != nil {
+			return nil, err
+		}
+
+		if err := GetFileContents(opts.TemplateOpts, opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
+			return nil, err
+		}
+		opts.TemplateOpts.FixFileRefs()
+		s["template"] = string(opts.TemplateOpts.Bin)
+
+		for k, v := range opts.TemplateOpts.Files {
+			Files[k] = v
+		}
+	}
+	if opts.DisableRollback != nil {
+		s["disable_rollback"] = &opts.DisableRollback
+	}
+
+	if opts.EnvironmentOpts != nil {
+		if err := opts.EnvironmentOpts.Parse(); err != nil {
+			return nil, err
+		}
+		if err := GetRRFileContents(opts.EnvironmentOpts, ignoreIfEnvironment); err != nil {
+			return nil, err
+		}
+		opts.EnvironmentOpts.FixFileRefs()
+		for k, v := range opts.EnvironmentOpts.Files {
+			Files[k] = v
+		}
+		s["environment"] = string(opts.EnvironmentOpts.Bin)
+	} else if opts.Environment != "" {
+		s["environment"] = opts.Environment
+	}
+
+	if opts.Files != nil {
+		s["files"] = opts.Files
+	} else {
+		s["files"] = Files
 	}
 
 	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
 	}
@@ -139,9 +187,16 @@
 	Name string
 	// (REQUIRED) The timeout for stack creation in minutes.
 	Timeout int
+	// (REQUIRED) A structure that contains either the template file or url. Call the
+	// associated methods to extract the information relevant to send in a create request.
+	TemplateOpts *Template
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, TemplateURL will be ignored
 	// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
 	// This value is ignored if Template is supplied inline.
 	TemplateURL string
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, Template will be ignored
 	// (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:
@@ -157,8 +212,14 @@
 	// creation fails. Default is true, meaning all resources are not deleted when
 	// stack creation fails.
 	DisableRollback Rollback
+	// (OPTIONAL) A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment
+	// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
 	// (OPTIONAL) A stringified JSON environment for the stack.
 	Environment string
+	// (DEPRECATED): Files is automatically determined
+	// by parsing the template and environment passed as TemplateOpts and
+	// EnvironmentOpts respectively.
 	// (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"}`
@@ -175,15 +236,30 @@
 		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
+	Files := make(map[string]string)
+	if opts.TemplateOpts == nil {
+		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.")
+		}
 	} else {
-		return s, errors.New("Either Template or TemplateURL must be provided.")
-	}
+		if err := opts.TemplateOpts.Parse(); err != nil {
+			return nil, err
+		}
 
+		if err := GetFileContents(opts.TemplateOpts, opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
+			return nil, err
+		}
+		opts.TemplateOpts.FixFileRefs()
+		s["template"] = string(opts.TemplateOpts.Bin)
+
+		for k, v := range opts.TemplateOpts.Files {
+			Files[k] = v
+		}
+	}
 	if opts.AdoptStackData == "" {
 		return s, errors.New("Required field 'AdoptStackData' not provided.")
 	}
@@ -193,12 +269,28 @@
 		s["disable_rollback"] = &opts.DisableRollback
 	}
 
-	if opts.Environment != "" {
+	if opts.EnvironmentOpts != nil {
+		if err := opts.EnvironmentOpts.Parse(); err != nil {
+			return nil, err
+		}
+		if err := GetRRFileContents(opts.EnvironmentOpts, ignoreIfEnvironment); err != nil {
+			return nil, err
+		}
+		opts.EnvironmentOpts.FixFileRefs()
+		for k, v := range opts.EnvironmentOpts.Files {
+			Files[k] = v
+		}
+		s["environment"] = string(opts.EnvironmentOpts.Bin)
+	} else if opts.Environment != "" {
 		s["environment"] = opts.Environment
 	}
+
 	if opts.Files != nil {
 		s["files"] = opts.Files
+	} else {
+		s["files"] = Files
 	}
+
 	if opts.Parameters != nil {
 		s["parameters"] = opts.Parameters
 	}
@@ -311,9 +403,16 @@
 // UpdateOpts contains the common options struct used in this package's Update
 // operation.
 type UpdateOpts struct {
+	// (REQUIRED) A structure that contains either the template file or url. Call the
+	// associated methods to extract the information relevant to send in a create request.
+	TemplateOpts *Template
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, TemplateURL will be ignored
 	// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
 	// This value is ignored if Template is supplied inline.
 	TemplateURL string
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, Template will be ignored
 	// (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:
@@ -325,8 +424,14 @@
 	// }
 	// opts.Template = string(b)
 	Template string
+	// (OPTIONAL) A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment
+	// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
 	// (OPTIONAL) A stringified JSON environment for the stack.
 	Environment string
+	// (DEPRECATED): Files is automatically determined
+	// by parsing the template and environment passed as TemplateOpts and
+	// EnvironmentOpts respectively.
 	// (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"}`
@@ -342,21 +447,51 @@
 // 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
+	Files := make(map[string]string)
+	if opts.TemplateOpts == nil {
+		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.")
+		}
 	} else {
-		return s, errors.New("Either Template or TemplateURL must be provided.")
+		if err := opts.TemplateOpts.Parse(); err != nil {
+			return nil, err
+		}
+
+		if err := GetFileContents(opts.TemplateOpts, opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
+			return nil, err
+		}
+		opts.TemplateOpts.FixFileRefs()
+		s["template"] = string(opts.TemplateOpts.Bin)
+
+		for k, v := range opts.TemplateOpts.Files {
+			Files[k] = v
+		}
 	}
 
-	if opts.Environment != "" {
+	if opts.EnvironmentOpts != nil {
+		if err := opts.EnvironmentOpts.Parse(); err != nil {
+			return nil, err
+		}
+		if err := GetRRFileContents(opts.EnvironmentOpts, ignoreIfEnvironment); err != nil {
+			return nil, err
+		}
+		opts.EnvironmentOpts.FixFileRefs()
+		for k, v := range opts.EnvironmentOpts.Files {
+			Files[k] = v
+		}
+		s["environment"] = string(opts.EnvironmentOpts.Bin)
+	} else if opts.Environment != "" {
 		s["environment"] = opts.Environment
 	}
 
 	if opts.Files != nil {
 		s["files"] = opts.Files
+	} else {
+		s["files"] = Files
 	}
 
 	if opts.Parameters != nil {
@@ -409,9 +544,16 @@
 	Name string
 	// (REQUIRED) The timeout for stack creation in minutes.
 	Timeout int
+	// (REQUIRED) A structure that contains either the template file or url. Call the
+	// associated methods to extract the information relevant to send in a create request.
+	TemplateOpts *Template
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, TemplateURL will be ignored
 	// (OPTIONAL; REQUIRED IF Template IS EMPTY) The URL of the template to instantiate.
 	// This value is ignored if Template is supplied inline.
 	TemplateURL string
+	// (DEPRECATED): Please use TemplateOpts for providing the template. If
+	// TemplateOpts is provided, Template will be ignored
 	// (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:
@@ -427,8 +569,14 @@
 	// creation fails. Default is true, meaning all resources are not deleted when
 	// stack creation fails.
 	DisableRollback Rollback
+	// (OPTIONAL) A structure that contains details for the environment of the stack.
+	EnvironmentOpts *Environment
+	// (DEPRECATED): Please use EnvironmentOpts to provide Environment data
 	// (OPTIONAL) A stringified JSON environment for the stack.
 	Environment string
+	// (DEPRECATED): Files is automatically determined
+	// by parsing the template and environment passed as TemplateOpts and
+	// EnvironmentOpts respectively.
 	// (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"}`
@@ -445,25 +593,56 @@
 		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
+	Files := make(map[string]string)
+	if opts.TemplateOpts == nil {
+		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.")
+		}
 	} else {
-		return s, errors.New("Either Template or TemplateURL must be provided.")
-	}
+		if err := opts.TemplateOpts.Parse(); err != nil {
+			return nil, err
+		}
 
+		if err := GetFileContents(opts.TemplateOpts, opts.TemplateOpts.Parsed, ignoreIfTemplate, true); err != nil {
+			return nil, err
+		}
+		opts.TemplateOpts.FixFileRefs()
+		s["template"] = string(opts.TemplateOpts.Bin)
+
+		for k, v := range opts.TemplateOpts.Files {
+			Files[k] = v
+		}
+	}
 	if opts.DisableRollback != nil {
 		s["disable_rollback"] = &opts.DisableRollback
 	}
 
-	if opts.Environment != "" {
+	if opts.EnvironmentOpts != nil {
+		if err := opts.EnvironmentOpts.Parse(); err != nil {
+			return nil, err
+		}
+		if err := GetRRFileContents(opts.EnvironmentOpts, ignoreIfEnvironment); err != nil {
+			return nil, err
+		}
+		opts.EnvironmentOpts.FixFileRefs()
+		for k, v := range opts.EnvironmentOpts.Files {
+			Files[k] = v
+		}
+		s["environment"] = string(opts.EnvironmentOpts.Bin)
+	} else if opts.Environment != "" {
 		s["environment"] = opts.Environment
 	}
+
 	if opts.Files != nil {
 		s["files"] = opts.Files
+	} else {
+		s["files"] = Files
 	}
+
 	if opts.Parameters != nil {
 		s["parameters"] = opts.Parameters
 	}
diff --git a/openstack/orchestration/v1/stacks/requests_test.go b/openstack/orchestration/v1/stacks/requests_test.go
index 1606d98..0fde44b 100644
--- a/openstack/orchestration/v1/stacks/requests_test.go
+++ b/openstack/orchestration/v1/stacks/requests_test.go
@@ -52,6 +52,35 @@
 	th.AssertDeepEquals(t, expected, actual)
 }
 
+func TestCreateStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateSuccessfully(t, CreateOutput)
+	template := new(Template)
+	template.Bin = []byte(`
+		{
+			"heat_template_version": "2013-05-23",
+			"description": "Simple template to test heat commands",
+			"parameters": {
+				"flavor": {
+					"default": "m1.tiny",
+					"type": "string"
+				}
+			}
+		}`)
+	createOpts := CreateOpts{
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    template,
+		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()
@@ -97,6 +126,52 @@
 	th.AssertDeepEquals(t, expected, actual)
 }
 
+func TestAdoptStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleCreateSuccessfully(t, CreateOutput)
+	template := new(Template)
+	template.Bin = []byte(`
+{
+  "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"
+		}
+	  }
+	}
+  }
+}`)
+	adoptOpts := AdoptOpts{
+		AdoptStackData:  `{environment{parameters{}}}`,
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    template,
+		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()
@@ -163,6 +238,30 @@
 	th.AssertNoErr(t, err)
 }
 
+func TestUpdateStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleUpdateSuccessfully(t)
+
+	template := new(Template)
+	template.Bin = []byte(`
+		{
+			"heat_template_version": "2013-05-23",
+			"description": "Simple template to test heat commands",
+			"parameters": {
+				"flavor": {
+					"default": "m1.tiny",
+					"type": "string"
+				}
+			}
+		}`)
+	updateOpts := UpdateOpts{
+		TemplateOpts: template,
+	}
+	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()
@@ -216,6 +315,36 @@
 	th.AssertDeepEquals(t, expected, actual)
 }
 
+func TestPreviewStackNewTemplateFormat(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePreviewSuccessfully(t, GetOutput)
+
+	template := new(Template)
+	template.Bin = []byte(`
+		{
+			"heat_template_version": "2013-05-23",
+			"description": "Simple template to test heat commands",
+			"parameters": {
+				"flavor": {
+					"default": "m1.tiny",
+					"type": "string"
+				}
+			}
+		}`)
+	previewOpts := PreviewOpts{
+		Name:            "stackcreated",
+		Timeout:         60,
+		TemplateOpts:    template,
+		DisableRollback: Disable,
+	}
+	actual, err := Preview(fake.ServiceClient(), previewOpts).Extract()
+	th.AssertNoErr(t, err)
+
+	expected := PreviewExpected
+	th.AssertDeepEquals(t, expected, actual)
+}
+
 func TestAbandonStack(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/openstack/orchestration/v1/stacks/template.go b/openstack/orchestration/v1/stacks/template.go
new file mode 100644
index 0000000..500d46c
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/template.go
@@ -0,0 +1,113 @@
+package stacks
+
+import (
+	"errors"
+	"fmt"
+	"github.com/rackspace/gophercloud"
+	"reflect"
+	"strings"
+)
+
+type Template struct {
+	TE
+}
+
+var TemplateFormatVersions = map[string]bool{
+	"HeatTemplateFormatVersion": true,
+	"heat_template_version":     true,
+	"AWSTemplateFormatVersion":  true,
+}
+
+func (t *Template) Validate() error {
+	if t.Parsed == nil {
+		if err := t.Parse(); err != nil {
+			return err
+		}
+	}
+	for key, _ := range t.Parsed {
+		if _, ok := TemplateFormatVersions[key]; ok {
+			return nil
+		}
+	}
+	return errors.New(fmt.Sprintf("Template format version not found."))
+}
+
+func GetFileContents(t *Template, te interface{}, ignoreIf igFunc, recurse bool) error {
+	if t.Files == nil {
+		t.Files = make(map[string]string)
+	}
+	if t.fileMaps == nil {
+		t.fileMaps = make(map[string]string)
+	}
+	switch te.(type) {
+	case map[string]interface{}, map[interface{}]interface{}:
+		te_map, err := toStringKeys(te)
+		if err != nil {
+			return err
+		}
+		for k, v := range te_map {
+			value, ok := v.(string)
+			if !ok {
+				if err := GetFileContents(t, v, ignoreIf, recurse); err != nil {
+					return err
+				}
+			} else if !ignoreIf(k, value) {
+				// at this point, the k, v pair has a reference to an external template.
+				// The assumption of heatclient is that value v is a relative reference
+				// to a file in the users environment
+				childTemplate := new(Template)
+				baseURL, err := gophercloud.NormalizePathURL(t.baseURL, value)
+				if err != nil {
+					return err
+				}
+				childTemplate.baseURL = baseURL
+				childTemplate.client = t.client
+				if err := childTemplate.Parse(); err != nil {
+					return err
+				}
+				// process child template recursively if required
+				if recurse {
+					if err := GetFileContents(childTemplate, childTemplate.Parsed, ignoreIf, recurse); err != nil {
+						return err
+					}
+				}
+				// update parent template with current child templates' content
+				t.fileMaps[value] = childTemplate.URL
+				t.Files[childTemplate.URL] = string(childTemplate.Bin)
+
+			}
+		}
+		return nil
+	case []interface{}:
+		te_slice := te.([]interface{})
+		for i := range te_slice {
+			if err := GetFileContents(t, te_slice[i], ignoreIf, recurse); err != nil {
+				return err
+			}
+		}
+	case string, bool, float64, nil, int:
+		return nil
+	default:
+		return errors.New(fmt.Sprintf("%v: Unrecognized type", reflect.TypeOf(te)))
+
+	}
+	return nil
+}
+
+// function to choose keys whose values are other template files
+func ignoreIfTemplate(key string, value interface{}) bool {
+	// key must be either `get_file` or `type` for value to be a URL
+	if key != "get_file" && key != "type" {
+		return true
+	}
+	// value must be a string
+	valueString, ok := value.(string)
+	if !ok {
+		return true
+	}
+	// `.template` and `.yaml` are allowed suffixes for template URLs when referred to by `type`
+	if key == "type" && !(strings.HasSuffix(valueString, ".template") || strings.HasSuffix(valueString, ".yaml")) {
+		return true
+	}
+	return false
+}
diff --git a/openstack/orchestration/v1/stacks/template_test.go b/openstack/orchestration/v1/stacks/template_test.go
new file mode 100644
index 0000000..23e2cc9
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/template_test.go
@@ -0,0 +1,148 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTemplateValidation(t *testing.T) {
+	templateJSON := new(Template)
+	templateJSON.Bin = []byte(ValidJSONTemplate)
+	err := templateJSON.Validate()
+	th.AssertNoErr(t, err)
+
+	templateYAML := new(Template)
+	templateYAML.Bin = []byte(ValidYAMLTemplate)
+	err = templateYAML.Validate()
+	th.AssertNoErr(t, err)
+
+	templateInvalid := new(Template)
+	templateInvalid.Bin = []byte(InvalidTemplateNoVersion)
+	if err = templateInvalid.Validate(); err == nil {
+		t.Error("Template validation did not catch invalid template")
+	}
+}
+
+func TestTemplateParsing(t *testing.T) {
+	templateJSON := new(Template)
+	templateJSON.Bin = []byte(ValidJSONTemplate)
+	err := templateJSON.Parse()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, ValidJSONTemplateParsed, templateJSON.Parsed)
+
+	templateYAML := new(Template)
+	templateYAML.Bin = []byte(ValidJSONTemplate)
+	err = templateYAML.Parse()
+	th.AssertNoErr(t, err)
+	th.AssertDeepEquals(t, ValidJSONTemplateParsed, templateYAML.Parsed)
+
+	templateInvalid := new(Template)
+	templateInvalid.Bin = []byte("Keep Austin Weird")
+	err = templateInvalid.Parse()
+	if err == nil {
+		t.Error("Template parsing did not catch invalid template")
+	}
+}
+
+func TestIgnoreIfTemplate(t *testing.T) {
+	var keyValueTests = []struct {
+		key   string
+		value interface{}
+		out   bool
+	}{
+		{"not_get_file", "afksdf", true},
+		{"not_type", "sdfd", true},
+		{"get_file", "shdfuisd", false},
+		{"type", "dfsdfsd", true},
+		{"type", "sdfubsduf.yaml", false},
+		{"type", "sdfsdufs.template", false},
+		{"type", "sdfsdf.file", true},
+		{"type", map[string]string{"key": "value"}, true},
+	}
+	var result bool
+	for _, kv := range keyValueTests {
+		result = ignoreIfTemplate(kv.key, kv.value)
+		if result != kv.out {
+			t.Errorf("key: %v, value: %v expected: %v, actual: %v", kv.key, kv.value, result, kv.out)
+		}
+	}
+}
+
+func TestGetFileContents(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	baseurl, err := getBasePath()
+	th.AssertNoErr(t, err)
+	fakeURL := strings.Join([]string{baseurl, "my_nova.yaml"}, "/")
+	urlparsed, err := url.Parse(fakeURL)
+	th.AssertNoErr(t, err)
+	my_nova_content := `heat_template_version: 2014-10-16
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+      networks:
+      - {uuid: 11111111-1111-1111-1111-111111111111}`
+	th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		w.Header().Set("Content-Type", "application/jason")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, my_nova_content)
+	})
+
+	client := fakeClient{BaseClient: getHTTPClient()}
+	te := new(Template)
+	te.Bin = []byte(`heat_template_version: 2015-04-30
+resources:
+  my_server:
+    type: my_nova.yaml`)
+	te.client = client
+
+	err = te.Parse()
+	th.AssertNoErr(t, err)
+	err = GetFileContents(te, te.Parsed, ignoreIfTemplate, true)
+	th.AssertNoErr(t, err)
+	expected_files := map[string]string{
+		"my_nova.yaml": `heat_template_version: 2014-10-16
+parameters:
+  flavor:
+    type: string
+    description: Flavor for the server to be created
+    default: 4353
+    hidden: true
+resources:
+  test_server:
+    type: "OS::Nova::Server"
+    properties:
+      name: test-server
+      flavor: 2 GB General Purpose v1
+      image: Debian 7 (Wheezy) (PVHVM)
+      networks:
+      - {uuid: 11111111-1111-1111-1111-111111111111}`}
+	th.AssertEquals(t, expected_files["my_nova.yaml"], te.Files[fakeURL])
+	te.FixFileRefs()
+	expected_parsed := map[string]interface{}{
+		"heat_template_version": "2015-04-30",
+		"resources": map[string]interface{}{
+			"my_server": map[string]interface{}{
+				"type": fakeURL,
+			},
+		},
+	}
+	te.Parse()
+	th.AssertDeepEquals(t, expected_parsed, te.Parsed)
+}
diff --git a/openstack/orchestration/v1/stacks/utils.go b/openstack/orchestration/v1/stacks/utils.go
new file mode 100644
index 0000000..2b80c6a
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/utils.go
@@ -0,0 +1,145 @@
+package stacks
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"path/filepath"
+	"reflect"
+	"strings"
+
+	"github.com/rackspace/gophercloud"
+	"gopkg.in/yaml.v2"
+)
+
+type Client interface {
+	Get(string) (*http.Response, error)
+}
+
+type TE struct {
+	// Bin stores the contents of the template or environment.
+	Bin []byte
+	// URL stores the URL of the template. This is allowed to be a 'file://'
+	// for local files.
+	URL string
+	// Parsed contains a parsed version of Bin. Since there are 2 different
+	// fields referring to the same value, you must be careful when accessing
+	// this filed.
+	Parsed map[string]interface{}
+	// Files contains a mapping between the urls in templates to their contents.
+	Files map[string]string
+	// fileMaps is a map used internally when determining Files.
+	fileMaps map[string]string
+	// baseURL represents the location of the template or environment file.
+	baseURL string
+	// client is an interface which allows TE to fetch contents from URLS
+	client Client
+}
+
+func (t *TE) Fetch() error {
+	// get baseURL if not already defined
+	if t.baseURL == "" {
+		u, err := getBasePath()
+		if err != nil {
+			return err
+		}
+		t.baseURL = u
+	}
+	if t.Bin != nil {
+		// already have contents
+		return nil
+	}
+	u, err := gophercloud.NormalizePathURL(t.baseURL, t.URL)
+	if err != nil {
+		return err
+	}
+	t.URL = u
+	// get an HTTP client if none present
+	if t.client == nil {
+		t.client = getHTTPClient()
+	}
+	resp, err := t.client.Get(t.URL)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+	t.Bin = body
+	return nil
+}
+
+// get the basepath of the template
+func getBasePath() (string, error) {
+	basePath, err := filepath.Abs(".")
+	if err != nil {
+		return "", err
+	}
+	u, err := gophercloud.NormalizePathURL("", basePath)
+	if err != nil {
+		return "", err
+	}
+	return u, nil
+}
+
+// get a an HTTP client to retrieve URLs
+func getHTTPClient() Client {
+	transport := &http.Transport{}
+	transport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
+	return &http.Client{Transport: transport}
+}
+
+// parse the contents and validate
+func (t *TE) Parse() error {
+	if err := t.Fetch(); err != nil {
+		return err
+	}
+	if jerr := json.Unmarshal(t.Bin, &t.Parsed); jerr != nil {
+		if yerr := yaml.Unmarshal(t.Bin, &t.Parsed); yerr != nil {
+			return errors.New(fmt.Sprintf("Data in neither json nor yaml format."))
+		}
+	}
+	return t.Validate()
+}
+
+// base Validate method, always returns true
+func (t *TE) Validate() error {
+	return nil
+}
+
+type igFunc func(string, interface{}) bool
+
+// convert map[interface{}]interface{} to map[string]interface{}
+func toStringKeys(m interface{}) (map[string]interface{}, error) {
+	switch m.(type) {
+	case map[string]interface{}, map[interface{}]interface{}:
+		typed_map := make(map[string]interface{})
+		if _, ok := m.(map[interface{}]interface{}); ok {
+			for k, v := range m.(map[interface{}]interface{}) {
+				typed_map[k.(string)] = v
+			}
+		} else {
+			typed_map = m.(map[string]interface{})
+		}
+		return typed_map, nil
+	default:
+		return nil, errors.New(fmt.Sprintf("Expected a map of type map[string]interface{} or map[interface{}]interface{}, actual type: %v", reflect.TypeOf(m)))
+
+	}
+}
+
+// fix the template reference to files
+func (t *TE) FixFileRefs() {
+	t_str := string(t.Bin)
+	if t.fileMaps == nil {
+		return
+	}
+	for k, v := range t.fileMaps {
+		t_str = strings.Replace(t_str, k, v, -1)
+	}
+	t.Bin = []byte(t_str)
+}
diff --git a/openstack/orchestration/v1/stacks/utils_test.go b/openstack/orchestration/v1/stacks/utils_test.go
new file mode 100644
index 0000000..2f01c3b
--- /dev/null
+++ b/openstack/orchestration/v1/stacks/utils_test.go
@@ -0,0 +1,94 @@
+package stacks
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestTEFixFileRefs(t *testing.T) {
+	te := TE{
+		Bin: []byte(`string_to_replace: my fair lady`),
+		fileMaps: map[string]string{
+			"string_to_replace": "london bridge is falling down",
+		},
+	}
+	te.FixFileRefs()
+	th.AssertEquals(t, string(te.Bin), `london bridge is falling down: my fair lady`)
+}
+
+func TesttoStringKeys(t *testing.T) {
+	var test_1 interface{} = map[interface{}]interface{}{
+		"Adam":  "Smith",
+		"Isaac": "Newton",
+	}
+	result_1, err := toStringKeys(test_1)
+	th.AssertNoErr(t, err)
+
+	expected := map[string]interface{}{
+		"Adam":  "Smith",
+		"Isaac": "Newton",
+	}
+	th.AssertDeepEquals(t, result_1, expected)
+}
+
+func TestGetBasePath(t *testing.T) {
+	_, err := getBasePath()
+	th.AssertNoErr(t, err)
+}
+
+// test if HTTP client can read file type URLS. Read the URL of this file
+// because if this test is running, it means this file _must_ exist
+func TestGetHTTPClient(t *testing.T) {
+	client := getHTTPClient()
+	baseurl, err := getBasePath()
+	th.AssertNoErr(t, err)
+	resp, err := client.Get(baseurl)
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, resp.StatusCode, 200)
+}
+
+// Implement a fakeclient that can be used to mock out HTTP requests
+type fakeClient struct {
+	BaseClient Client
+}
+
+// this client's Get method first changes the URL given to point to
+// testhelper's (th) endpoints. This is done because the http Mux does not seem
+// to work for fqdns with the `file` scheme
+func (c fakeClient) Get(url string) (*http.Response, error) {
+	newurl := strings.Replace(url, "file://", th.Endpoint(), 1)
+	return c.BaseClient.Get(newurl)
+}
+
+// test the fetch function
+func TestFetch(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	baseurl, err := getBasePath()
+	th.AssertNoErr(t, err)
+	fakeURL := strings.Join([]string{baseurl, "file.yaml"}, "/")
+	urlparsed, err := url.Parse(fakeURL)
+	th.AssertNoErr(t, err)
+
+	th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		w.Header().Set("Content-Type", "application/jason")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, "Fee-fi-fo-fum")
+	})
+
+	client := fakeClient{BaseClient: getHTTPClient()}
+	te := TE{
+		URL:    "file.yaml",
+		client: client,
+	}
+	err = te.Fetch()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, fakeURL, te.URL)
+	th.AssertEquals(t, "Fee-fi-fo-fum", string(te.Bin))
+}