images service v2 port from rackpsace/gophercloud (#171)

* CheckByteArrayEquals funcs

* direct port from rackspace/gophercloud with minor additions to get unit tests passing

* new package for uploading and downloading image data

* updates to make imageservice v2 consistent with the rest of gophercloud/gophercloud

* add image service v2 client
diff --git a/openstack/imageservice/v2/images/results.go b/openstack/imageservice/v2/images/results.go
new file mode 100644
index 0000000..653c68c
--- /dev/null
+++ b/openstack/imageservice/v2/images/results.go
@@ -0,0 +1,176 @@
+package images
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// Image model
+// Does not include the literal image data; just metadata.
+// returned by listing images, and by fetching a specific image.
+type Image struct {
+	// ID is the image UUID
+	ID string `json:"id"`
+
+	// Name is the human-readable display name for the image.
+	Name string `json:"name"`
+
+	// Status is the image status. It can be "queued" or "active"
+	// See imageservice/v2/images/type.go
+	Status ImageStatus `json:"status"`
+
+	// Tags is a list of image tags. Tags are arbitrarily defined strings
+	// attached to an image.
+	Tags []string `json:"tags"`
+
+	// ContainerFormat is the format of the container.
+	// Valid values are ami, ari, aki, bare, and ovf.
+	ContainerFormat string `json:"container_format"`
+
+	// DiskFormat is the format of the disk.
+	// If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, and iso.
+	DiskFormat string `json:"disk_format"`
+
+	// MinDiskGigabytes is the amount of disk space in GB that is required to boot the image.
+	MinDiskGigabytes int `json:"min_disk"`
+
+	// MinRAMMegabytes [optional] is the amount of RAM in MB that is required to boot the image.
+	MinRAMMegabytes int `json:"min_ram"`
+
+	// Owner is the tenant the image belongs to.
+	Owner string `json:"owner"`
+
+	// Protected is whether the image is deletable or not.
+	Protected bool `json:"protected"`
+
+	// Visibility defines who can see/use the image.
+	Visibility ImageVisibility `json:"visibility"`
+
+	// Checksum is the checksum of the data that's associated with the image
+	Checksum string `json:"checksum"`
+
+	// SizeBytes is the size of the data that's associated with the image.
+	SizeBytes int64 `json:"size"`
+
+	// Metadata is a set of metadata associated with the image.
+	// Image metadata allow for meaningfully define the image properties
+	// and tags. See http://docs.openstack.org/developer/glance/metadefs-concepts.html.
+	Metadata map[string]string `json:"metadata"`
+
+	// Properties is a set of key-value pairs, if any, that are associated with the image.
+	Properties map[string]string `json:"properties"`
+
+	// CreatedAt is the date when the image has been created.
+	CreatedAt time.Time `json:"-"`
+
+	// UpdatedAt is the date when the last change has been made to the image or it's properties.
+	UpdatedAt time.Time `json:"-"`
+
+	// File is the trailing path after the glance endpoint that represent the location
+	// of the image or the path to retrieve it.
+	File string `json:"file"`
+
+	// Schema is the path to the JSON-schema that represent the image or image entity.
+	Schema string `json:"schema"`
+}
+
+func (s *Image) UnmarshalJSON(b []byte) error {
+	type tmp Image
+	var p *struct {
+		tmp
+		SizeBytes interface{} `json:"size"`
+		CreatedAt string      `json:"created_at"`
+		UpdatedAt string      `json:"updated_at"`
+	}
+	err := json.Unmarshal(b, &p)
+	if err != nil {
+		return err
+	}
+	*s = Image(p.tmp)
+
+	switch t := p.SizeBytes.(type) {
+	case nil:
+		return nil
+	case float32:
+		s.SizeBytes = int64(t)
+	case float64:
+		s.SizeBytes = int64(t)
+	default:
+		return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t)
+	}
+
+	s.CreatedAt, err = time.Parse(time.RFC3339, p.CreatedAt)
+	if err != nil {
+		return err
+	}
+	s.UpdatedAt, err = time.Parse(time.RFC3339, p.UpdatedAt)
+	return err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets any commonResult as an Image.
+func (r commonResult) Extract() (*Image, error) {
+	var s *Image
+	err := r.ExtractInto(&s)
+	return s, err
+}
+
+// CreateResult represents the result of a Create operation
+type CreateResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an Update operation
+type UpdateResult struct {
+	commonResult
+}
+
+// GetResult represents the result of a Get operation
+type GetResult struct {
+	commonResult
+}
+
+//DeleteResult model
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// ImagePage represents page
+type ImagePage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no Images results.
+func (r ImagePage) IsEmpty() (bool, error) {
+	images, err := ExtractImages(r)
+	return len(images) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
+func (r ImagePage) NextPageURL() (string, error) {
+	var s struct {
+		Next string `json:"next"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return nextPageURL(r.URL.String(), s.Next), nil
+}
+
+// ExtractImages interprets the results of a single page from a List() call, producing a slice of Image entities.
+func ExtractImages(r pagination.Page) ([]Image, error) {
+	var s struct {
+		Images []Image `json:"images"`
+	}
+	err := (r.(ImagePage)).ExtractInto(&s)
+	return s.Images, err
+}