ImageService v2: Collect all properties of images (#376)

* ImageService v2: Add VirtualSize field

* ImageService v2: Have Images.Properties collect all remaining fields

Related-PROD: PROD-28126

Change-Id: Ib6311d3bafc1e5e6e6a2c6d043d2a63a1eaa96cf
diff --git a/acceptance/openstack/imageservice/v2/images_test.go b/acceptance/openstack/imageservice/v2/images_test.go
index 5fe1c16..7f06faa 100644
--- a/acceptance/openstack/imageservice/v2/images_test.go
+++ b/acceptance/openstack/imageservice/v2/images_test.go
@@ -7,8 +7,62 @@
 
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git/acceptance/clients"
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git/acceptance/tools"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/openstack/imageservice/v2/images"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
 )
 
+func TestImagesListEachPage(t *testing.T) {
+	client, err := clients.NewImageServiceV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create an image service client: %v", err)
+	}
+
+	listOpts := images.ListOpts{
+		Limit: 1,
+	}
+
+	pager := images.List(client, listOpts)
+	err = pager.EachPage(func(page pagination.Page) (bool, error) {
+		images, err := images.ExtractImages(page)
+		if err != nil {
+			t.Fatalf("Unable to extract images: %v", err)
+		}
+
+		for _, image := range images {
+			tools.PrintResource(t, image)
+			tools.PrintResource(t, image.Properties)
+		}
+
+		return true, nil
+	})
+}
+
+func TestImagesListAllPages(t *testing.T) {
+	client, err := clients.NewImageServiceV2Client()
+	if err != nil {
+		t.Fatalf("Unable to create an image service client: %v", err)
+	}
+
+	listOpts := images.ListOpts{
+		Limit: 1,
+	}
+
+	allPages, err := images.List(client, listOpts).AllPages()
+	if err != nil {
+		t.Fatalf("Unable to retrieve all images: %v", err)
+	}
+
+	allImages, err := images.ExtractImages(allPages)
+	if err != nil {
+		t.Fatalf("Unable to extract images: %v", err)
+	}
+
+	for _, image := range allImages {
+		tools.PrintResource(t, image)
+		tools.PrintResource(t, image.Properties)
+	}
+}
+
 func TestImagesCreateDestroyEmptyImage(t *testing.T) {
 	client, err := clients.NewImageServiceV2Client()
 	if err != nil {
diff --git a/openstack/imageservice/v2/images/results.go b/openstack/imageservice/v2/images/results.go
index 8f46463..e670076 100644
--- a/openstack/imageservice/v2/images/results.go
+++ b/openstack/imageservice/v2/images/results.go
@@ -7,6 +7,7 @@
 	"time"
 
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git"
+	"gerrit.mcp.mirantis.net/debian/gophercloud.git/internal"
 	"gerrit.mcp.mirantis.net/debian/gophercloud.git/pagination"
 )
 
@@ -63,7 +64,7 @@
 	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"`
+	Properties map[string]interface{} `json:"-"`
 
 	// CreatedAt is the date when the image has been created.
 	CreatedAt time.Time `json:"created_at"`
@@ -77,6 +78,9 @@
 
 	// Schema is the path to the JSON-schema that represent the image or image entity.
 	Schema string `json:"schema"`
+
+	// VirtualSize is the virtual size of the image
+	VirtualSize int64 `json:"virtual_size"`
 }
 
 func (r *Image) UnmarshalJSON(b []byte) error {
@@ -102,6 +106,17 @@
 		return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t)
 	}
 
+	// Bundle all other fields into Properties
+	var result interface{}
+	err = json.Unmarshal(b, &result)
+	if err != nil {
+		return err
+	}
+	if resultMap, ok := result.(map[string]interface{}); ok {
+		delete(resultMap, "self")
+		r.Properties = internal.RemainingKeys(Image{}, resultMap)
+	}
+
 	return err
 }
 
diff --git a/openstack/imageservice/v2/images/testing/fixtures.go b/openstack/imageservice/v2/images/testing/fixtures.go
index 0f74929..ca0987d 100644
--- a/openstack/imageservice/v2/images/testing/fixtures.go
+++ b/openstack/imageservice/v2/images/testing/fixtures.go
@@ -43,7 +43,10 @@
             "owner": "cba624273b8344e59dd1fd18685183b0",
             "virtual_size": null,
             "min_ram": 0,
-            "schema": "/v2/schemas/image"
+            "schema": "/v2/schemas/image",
+            "hw_disk_bus": "scsi",
+            "hw_disk_bus_model": "virtio-scsi",
+            "hw_scsi_model": "virtio-scsi"
         }`}
 	images[1] = imageEntry{"cirros-0.3.4-x86_64-uec-ramdisk",
 		`{
@@ -65,7 +68,10 @@
             "owner": "cba624273b8344e59dd1fd18685183b0",
             "virtual_size": null,
             "min_ram": 0,
-            "schema": "/v2/schemas/image"
+            "schema": "/v2/schemas/image",
+            "hw_disk_bus": "scsi",
+            "hw_disk_bus_model": "virtio-scsi",
+            "hw_scsi_model": "virtio-scsi"
         }`}
 	images[2] = imageEntry{"cirros-0.3.4-x86_64-uec-kernel",
 		`{
@@ -87,7 +93,10 @@
             "owner": "cba624273b8344e59dd1fd18685183b0",
             "virtual_size": null,
             "min_ram": 0,
-            "schema": "/v2/schemas/image"
+            "schema": "/v2/schemas/image",
+            "hw_disk_bus": "scsi",
+            "hw_disk_bus_model": "virtio-scsi",
+            "hw_scsi_model": "virtio-scsi"
         }`}
 
 	th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) {
@@ -187,7 +196,10 @@
 			"schema": "/v2/schemas/image",
 			"size": 0,
 			"checksum": "",
-			"virtual_size": 0
+			"virtual_size": 0,
+			"hw_disk_bus": "scsi",
+			"hw_disk_bus_model": "virtio-scsi",
+			"hw_scsi_model": "virtio-scsi"
 		}`)
 	})
 }
@@ -261,7 +273,10 @@
 			"size": 13167616,
 			"min_ram": 0,
 			"schema": "/v2/schemas/image",
-			"virtual_size": "None"
+			"virtual_size": null,
+			"hw_disk_bus": "scsi",
+			"hw_disk_bus_model": "virtio-scsi",
+			"hw_scsi_model": "virtio-scsi"
 		}`)
 	})
 }
@@ -323,7 +338,10 @@
 			"min_disk": 0,
 			"disk_format": "",
 			"virtual_size": 0,
-			"container_format": ""
+			"container_format": "",
+			"hw_disk_bus": "scsi",
+			"hw_disk_bus_model": "virtio-scsi",
+			"hw_scsi_model": "virtio-scsi"
 		}`)
 	})
 }
diff --git a/openstack/imageservice/v2/images/testing/requests_test.go b/openstack/imageservice/v2/images/testing/requests_test.go
index e8f7d6a..ad4b88b 100644
--- a/openstack/imageservice/v2/images/testing/requests_test.go
+++ b/openstack/imageservice/v2/images/testing/requests_test.go
@@ -102,11 +102,17 @@
 
 		Owner: owner,
 
-		Visibility: images.ImageVisibilityPrivate,
-		File:       file,
-		CreatedAt:  createdDate,
-		UpdatedAt:  lastUpdate,
-		Schema:     schema,
+		Visibility:  images.ImageVisibilityPrivate,
+		File:        file,
+		CreatedAt:   createdDate,
+		UpdatedAt:   lastUpdate,
+		Schema:      schema,
+		VirtualSize: 0,
+		Properties: map[string]interface{}{
+			"hw_disk_bus":       "scsi",
+			"hw_disk_bus_model": "virtio-scsi",
+			"hw_scsi_model":     "virtio-scsi",
+		},
 	}
 
 	th.AssertDeepEquals(t, &expectedImage, actualImage)
@@ -204,12 +210,18 @@
 		Protected:  false,
 		Visibility: images.ImageVisibilityPublic,
 
-		Checksum:  checksum,
-		SizeBytes: sizeBytes,
-		File:      file,
-		CreatedAt: createdDate,
-		UpdatedAt: lastUpdate,
-		Schema:    schema,
+		Checksum:    checksum,
+		SizeBytes:   sizeBytes,
+		File:        file,
+		CreatedAt:   createdDate,
+		UpdatedAt:   lastUpdate,
+		Schema:      schema,
+		VirtualSize: 0,
+		Properties: map[string]interface{}{
+			"hw_disk_bus":       "scsi",
+			"hw_disk_bus_model": "virtio-scsi",
+			"hw_scsi_model":     "virtio-scsi",
+		},
 	}
 
 	th.AssertDeepEquals(t, &expectedImage, actualImage)
@@ -269,6 +281,12 @@
 		CreatedAt:       createdDate,
 		UpdatedAt:       lastUpdate,
 		Schema:          schema,
+		VirtualSize:     0,
+		Properties: map[string]interface{}{
+			"hw_disk_bus":       "scsi",
+			"hw_disk_bus_model": "virtio-scsi",
+			"hw_scsi_model":     "virtio-scsi",
+		},
 	}
 
 	th.AssertDeepEquals(t, &expectedImage, actualImage)