diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/config/NovaParserModule.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/config/NovaParserModule.java index f35dba88e3..fd0c8bd7e0 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/config/NovaParserModule.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/config/NovaParserModule.java @@ -18,17 +18,25 @@ package org.jclouds.openstack.nova.v2_0.config; import java.beans.ConstructorProperties; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import javax.inject.Singleton; +import com.google.common.collect.Maps; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; import org.jclouds.javax.annotation.Nullable; import org.jclouds.json.config.GsonModule; import org.jclouds.json.config.GsonModule.DateAdapter; import org.jclouds.openstack.nova.v2_0.domain.Address; +import org.jclouds.openstack.nova.v2_0.domain.BlockDeviceMapping; import org.jclouds.openstack.nova.v2_0.domain.HostResourceUsage; +import org.jclouds.openstack.nova.v2_0.domain.Image; import org.jclouds.openstack.nova.v2_0.domain.Server; import org.jclouds.openstack.nova.v2_0.domain.ServerExtendedAttributes; import org.jclouds.openstack.nova.v2_0.domain.ServerExtendedStatus; @@ -56,9 +64,10 @@ public class NovaParserModule extends AbstractModule { @Singleton public Map provideCustomAdapterBindings() { return ImmutableMap.of( - HostResourceUsage.class, new HostResourceUsageAdapter(), - ServerWithSecurityGroups.class, new ServerWithSecurityGroupsAdapter(), - Server.class, new ServerAdapter() + HostResourceUsage.class, new HostResourceUsageAdapter(), + ServerWithSecurityGroups.class, new ServerWithSecurityGroupsAdapter(), + Server.class, new ServerAdapter(), + Image.class, new ImageAdapter() ); } @@ -90,7 +99,7 @@ public class NovaParserModule extends AbstractModule { private static class HostResourceUsageInternal extends HostResourceUsage { @ConstructorProperties({ - "host", "project", "memory_mb", "cpu", "disk_gb" + "host", "project", "memory_mb", "cpu", "disk_gb" }) protected HostResourceUsageInternal(String host, @Nullable String project, int memoryMb, int cpu, int diskGb) { super(host, project, memoryMb, cpu, diskGb); @@ -102,7 +111,7 @@ public class NovaParserModule extends AbstractModule { public static class ServerWithSecurityGroupsAdapter implements JsonDeserializer { @Override public ServerWithSecurityGroups deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) - throws JsonParseException { + throws JsonParseException { Server server = context.deserialize(jsonElement, Server.class); ServerWithSecurityGroups.Builder result = ServerWithSecurityGroups.builder().fromServer(server); Set names = Sets.newLinkedHashSet(); @@ -121,7 +130,7 @@ public class NovaParserModule extends AbstractModule { public static class ServerAdapter implements JsonDeserializer { @Override public Server deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) - throws JsonParseException { + throws JsonParseException { Server serverBase; // Servers can be created without an image so test if an image object is returned @@ -149,7 +158,7 @@ public class NovaParserModule extends AbstractModule { private static class ServerInternal extends Server { @ConstructorProperties({ - "id", "name", "links", "uuid", "tenant_id", "user_id", "updated", "created", "hostId", "accessIPv4", "accessIPv6", "status", "image", "flavor", "key_name", "config_drive", "addresses", "metadata", "extendedStatus", "extendedAttributes", "OS-DCF:diskConfig" + "id", "name", "links", "uuid", "tenant_id", "user_id", "updated", "created", "hostId", "accessIPv4", "accessIPv6", "status", "image", "flavor", "key_name", "config_drive", "addresses", "metadata", "extendedStatus", "extendedAttributes", "OS-DCF:diskConfig" }) protected ServerInternal(String id, @Nullable String name, java.util.Set links, @Nullable String uuid, String tenantId, String userId, Date updated, Date created, @Nullable String hostId, @Nullable String accessIPv4, @@ -162,15 +171,72 @@ public class NovaParserModule extends AbstractModule { private static class ServerInternalWithoutImage extends Server { @ConstructorProperties({ - "id", "name", "links", "uuid", "tenant_id", "user_id", "updated", "created", "hostId", "accessIPv4", "accessIPv6", "status", "flavor", "key_name", "config_drive", "addresses", "metadata", "extendedStatus", "extendedAttributes", "OS-DCF:diskConfig" + "id", "name", "links", "uuid", "tenant_id", "user_id", "updated", "created", "hostId", "accessIPv4", "accessIPv6", "status", "flavor", "key_name", "config_drive", "addresses", "metadata", "extendedStatus", "extendedAttributes", "OS-DCF:diskConfig" }) protected ServerInternalWithoutImage(String id, @Nullable String name, java.util.Set links, @Nullable String uuid, String tenantId, - String userId, Date updated, Date created, @Nullable String hostId, @Nullable String accessIPv4, - @Nullable String accessIPv6, Server.Status status, Resource flavor, @Nullable String keyName, - @Nullable String configDrive, Multimap addresses, Map metadata, - @Nullable ServerExtendedStatus extendedStatus, @Nullable ServerExtendedAttributes extendedAttributes, @Nullable String diskConfig) { + String userId, Date updated, Date created, @Nullable String hostId, @Nullable String accessIPv4, + @Nullable String accessIPv6, Server.Status status, Resource flavor, @Nullable String keyName, + @Nullable String configDrive, Multimap addresses, Map metadata, + @Nullable ServerExtendedStatus extendedStatus, @Nullable ServerExtendedAttributes extendedAttributes, @Nullable String diskConfig) { super(id, name, links, uuid, tenantId, userId, updated, created, hostId, accessIPv4, accessIPv6, status, null, flavor, keyName, configDrive, addresses, metadata, extendedStatus, extendedAttributes, diskConfig); } } } + + @Singleton + public static class ImageAdapter implements JsonDeserializer { + public static final String METADATA = "metadata"; + public static final String BLOCK_DEVICE_MAPPING = "block_device_mapping"; + + @Override + public Image deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) + throws JsonParseException { + JsonObject json = jsonElement.getAsJsonObject(); + Map metadata = null; + List blockDeviceMapping = null; + + JsonElement meta = json.get(METADATA); + if (meta != null && meta.isJsonObject()) { + metadata = Maps.newTreeMap(); + for (Map.Entry e : meta.getAsJsonObject().entrySet()) { + Object value; + if (e.getValue().isJsonArray()) { + value = context.deserialize(e.getValue().getAsJsonArray(), ArrayList.class); + } else if (e.getValue().isJsonObject()) { + value = context.deserialize(e.getValue().getAsJsonObject(), TreeMap.class); + } else if (e.getValue().isJsonPrimitive()) { + value = e.getValue().getAsJsonPrimitive().getAsString(); + } else { + continue; + } + + //keep non-string members out of normal metadata + if (value instanceof String) { + metadata.put(e.getKey(), (String) value); + } else if (value instanceof List && BLOCK_DEVICE_MAPPING.equals(e.getKey())) { + blockDeviceMapping = context.deserialize(e.getValue(), new TypeToken>(){}.getType()); + } + } + json.remove(METADATA); + } + + return apply(context.deserialize(json, ImageInternal.class), metadata, blockDeviceMapping); + } + + public Image apply(ImageInternal in, Map metadata, List blockDeviceMapping) { + return in.toBuilder().metadata(metadata).blockDeviceMapping(blockDeviceMapping).build(); + } + + private static class ImageInternal extends Image { + @ConstructorProperties({ + "id", "name", "links", "updated", "created", "tenant_id", "user_id", "status", "progress", "minDisk", "minRam", "blockDeviceMapping", "server", "metadata" + }) + protected ImageInternal(String id, @Nullable String name, java.util.Set links, @Nullable Date updated, @Nullable Date created, + String tenantId, @Nullable String userId, @Nullable Status status, int progress, int minDisk, int minRam, + @Nullable List blockDeviceMapping, @Nullable Resource server, @Nullable Map metadata) { + super(id, name, links, updated, created, tenantId, userId, status, progress, minDisk, minRam, blockDeviceMapping, server, metadata); + + } + } + } } diff --git a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/domain/Image.java b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/domain/Image.java index 7a73154c7a..b7c47c8b6b 100644 --- a/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/domain/Image.java +++ b/apis/openstack-nova/src/main/java/org/jclouds/openstack/nova/v2_0/domain/Image.java @@ -20,10 +20,12 @@ import static com.google.common.base.Preconditions.checkNotNull; import java.beans.ConstructorProperties; import java.util.Date; +import java.util.List; import java.util.Map; import javax.inject.Named; +import com.google.common.collect.ImmutableList; import org.jclouds.javax.annotation.Nullable; import org.jclouds.openstack.v2_0.domain.Link; import org.jclouds.openstack.v2_0.domain.Resource; @@ -85,6 +87,7 @@ public class Image extends Resource { protected int minDisk; protected int minRam; protected Resource server; + protected List blockDeviceMapping = ImmutableList.of(); protected Map metadata = ImmutableMap.of(); /** @@ -159,6 +162,11 @@ public class Image extends Resource { return self(); } + public T blockDeviceMapping(List blockDeviceMapping){ + this.blockDeviceMapping = blockDeviceMapping; + return self(); + } + /** * @see Image#getMetadata() */ @@ -168,7 +176,7 @@ public class Image extends Resource { } public Image build() { - return new Image(id, name, links, updated, created, tenantId, userId, status, progress, minDisk, minRam, server, metadata); + return new Image(id, name, links, updated, created, tenantId, userId, status, progress, minDisk, minRam, blockDeviceMapping, server, metadata); } public T fromImage(Image in) { @@ -203,15 +211,16 @@ public class Image extends Resource { private final int progress; private final int minDisk; private final int minRam; + private final List blockDeviceMapping; private final Resource server; private final Map metadata; @ConstructorProperties({ - "id", "name", "links", "updated", "created", "tenant_id", "user_id", "status", "progress", "minDisk", "minRam", "server", "metadata" + "id", "name", "links", "updated", "created", "tenant_id", "user_id", "status", "progress", "minDisk", "minRam", "server", "blockDeviceMapping", "metadata" }) protected Image(String id, @Nullable String name, java.util.Set links, @Nullable Date updated, @Nullable Date created, String tenantId, @Nullable String userId, @Nullable Status status, int progress, int minDisk, int minRam, - @Nullable Resource server, @Nullable Map metadata) { + @Nullable List blockDeviceMapping, @Nullable Resource server, @Nullable Map metadata) { super(id, name, links); this.updated = updated; this.created = created; @@ -221,6 +230,7 @@ public class Image extends Resource { this.progress = progress; this.minDisk = minDisk; this.minRam = minRam; + this.blockDeviceMapping = blockDeviceMapping == null ? ImmutableList.of() : blockDeviceMapping; this.server = server; this.metadata = metadata == null ? ImmutableMap.of() : ImmutableMap.copyOf(metadata); } @@ -262,6 +272,11 @@ public class Image extends Resource { return this.minRam; } + @Nullable + public List getBlockDeviceMapping(){ + return this.blockDeviceMapping; + } + @Nullable public Resource getServer() { return this.server; diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/config/ImageAdapterTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/config/ImageAdapterTest.java new file mode 100644 index 0000000000..dd7e33bb47 --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/config/ImageAdapterTest.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.openstack.nova.v2_0.config; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.io.IOException; + +import org.jclouds.json.config.GsonModule; +import org.jclouds.openstack.nova.v2_0.domain.Image; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import com.google.gson.Gson; +import com.google.inject.Guice; +import com.google.inject.Injector; + +@Test(groups = "unit") +public class ImageAdapterTest { + + private Gson gson; + + @BeforeTest + public void setup() { + Injector injector = Guice.createInjector(new GsonModule(), new NovaParserModule()); + gson = injector.getInstance(Gson.class); + } + + public void testDeserializeWithBlockDeviceMappingAndMetadata() throws Exception { + ImageContainer container = gson.fromJson(stringFromResource("image_details_with_block_device_mapping.json"), ImageContainer.class); + + // Note that the block device mapping keys are removed from the metadata by the adapter. + assertNotNull(container.image.getMetadata()); + assertEquals(container.image.getMetadata().size(), 2); + assertEquals("Gold", container.image.getMetadata().get("ImageType")); + assertEquals("1.5", container.image.getMetadata().get("ImageVersion")); + + assertNotNull(container.image.getBlockDeviceMapping()); + assertEquals(container.image.getBlockDeviceMapping().size(), 1); + assertEquals(new Integer(2), getOnlyElement(container.image.getBlockDeviceMapping()).getBootIndex()); + assertEquals("snapshot", getOnlyElement(container.image.getBlockDeviceMapping()).getSourceType()); + } + + public void testDeserializeWithoutBlockDeviceMapping() throws Exception { + ImageContainer container = gson.fromJson(stringFromResource("image_details.json"), ImageContainer.class); + + assertNotNull(container.image.getMetadata()); + assertEquals(container.image.getMetadata().size(), 2); + assertEquals("Gold", container.image.getMetadata().get("ImageType")); + assertEquals("1.5", container.image.getMetadata().get("ImageVersion")); + + assertNotNull(container.image.getBlockDeviceMapping()); + assertEquals(0, container.image.getBlockDeviceMapping().size()); + } + + public void testDeserializeWithoutBlockDeviceMappingOrMetadata() throws Exception { + ImageContainer container = gson.fromJson(stringFromResource("image_details_without_metadata.json"), ImageContainer.class); + + assertNotNull(container.image.getMetadata()); + assertEquals(container.image.getMetadata().size(), 0); + assertNotNull(container.image.getBlockDeviceMapping()); + assertEquals(0, container.image.getBlockDeviceMapping().size()); + } + + private String stringFromResource(String resource) throws IOException { + return Resources.toString(Resources.getResource(resource), Charsets.UTF_8); + } + + // Note that the ImageApi methods use the "@SelectJson" annotation to unwrap the object inside the "image" key + // We use this container to deserialize the Image object to simulate that behavior and use a *real* json + // in the tests. + public static class ImageContainer { + public Image image; + } +} diff --git a/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ImageApiMockTest.java b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ImageApiMockTest.java new file mode 100644 index 0000000000..7971451b72 --- /dev/null +++ b/apis/openstack-nova/src/test/java/org/jclouds/openstack/nova/v2_0/features/ImageApiMockTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jclouds.openstack.nova.v2_0.features; + +import com.google.common.collect.FluentIterable; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import org.jclouds.openstack.nova.v2_0.NovaApi; +import org.jclouds.openstack.nova.v2_0.domain.BlockDeviceMapping; +import org.jclouds.openstack.nova.v2_0.domain.Image; +import org.jclouds.openstack.v2_0.internal.BaseOpenStackMockTest; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +@Test(groups = "unit") +public class ImageApiMockTest extends BaseOpenStackMockTest { + public void testImageWithBlockDeviceMapping() throws Exception { + MockWebServer server = mockOpenStackServer(); + server.enqueue(addCommonHeaders(new MockResponse().setBody(stringFromResource("/access.json")))); + server.enqueue(addCommonHeaders(new MockResponse().setBody(stringFromResource("/image_list_with_block_device_mapping.json")))); + + try { + NovaApi novaApi = api(server.getUrl("/").toString(), "openstack-nova"); + ImageApi imageApi = novaApi.getImageApiForZone("RegionOne"); + + FluentIterable images = imageApi.listInDetail().concat(); + + Image img = images.get(0); + assertNotNull(img.getMetadata()); + assertEquals(10, img.getMetadata().size()); + assertNotNull(img.getBlockDeviceMapping()); + assertEquals(1, img.getBlockDeviceMapping().size()); + BlockDeviceMapping blockDeviceMapping = img.getBlockDeviceMapping().get(0); + assertEquals("snapshot", blockDeviceMapping.getSourceType()); + assertEquals(new Integer(2), blockDeviceMapping.getBootIndex()); + } finally { + server.shutdown(); + } + } +} diff --git a/apis/openstack-nova/src/test/resources/image_details_with_block_device_mapping.json b/apis/openstack-nova/src/test/resources/image_details_with_block_device_mapping.json new file mode 100644 index 0000000000..773a5939f2 --- /dev/null +++ b/apis/openstack-nova/src/test/resources/image_details_with_block_device_mapping.json @@ -0,0 +1,57 @@ +{ + "image": { + "id": "52415800-8b69-11e0-9b19-734f5736d2a2", + "name": "My Server Backup", + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "tenant_id": "12345", + "user_id": "joe", + "status": "SAVING", + "progress": 80, + "minDisk": 5, + "minRam": 256, + "metadata": { + "ImageType": "Gold", + "ImageVersion": 1.5, + "block_device_mapping": [ + { + "guest_format": null, + "boot_index": 2, + "no_device": null, + "volume_id": null, + "volume_size": null, + "disk_bus": null, + "image_id": null, + "source_type": "snapshot", + "device_type": null, + "snapshot_id": "a900a56c-61b7-4438-9150-76312fa1aa10", + "destination_type": "volume", + "delete_on_termination": null + } + ] + }, + "server": { + "id": "52415800-8b69-11e0-9b19-734f335aa7b3", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v2/1234/servers/52415800-8b69-11e0-9b19-734f335aa7b3" + }, + { + "rel": "bookmark", + "href": "http://servers.api.openstack.org/1234/servers/52415800-8b69-11e0-9b19-734f335aa7b3" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v2/1234/images/52415800-8b69-11e0-9b19-734f5736d2a2" + }, + { + "rel": "bookmark", + "href": "http://servers.api.openstack.org/1234/images/52415800-8b69-11e0-9b19-734f5736d2a2" + } + ] + } +} diff --git a/apis/openstack-nova/src/test/resources/image_details_without_metadata.json b/apis/openstack-nova/src/test/resources/image_details_without_metadata.json new file mode 100644 index 0000000000..82eb24a7df --- /dev/null +++ b/apis/openstack-nova/src/test/resources/image_details_without_metadata.json @@ -0,0 +1,38 @@ +{ + "image": { + "id": "52415800-8b69-11e0-9b19-734f5736d2a2", + "name": "My Server Backup", + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "tenant_id": "12345", + "user_id": "joe", + "status": "SAVING", + "progress": 80, + "minDisk": 5, + "minRam": 256, + "metadata": {}, + "server": { + "id": "52415800-8b69-11e0-9b19-734f335aa7b3", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v2/1234/servers/52415800-8b69-11e0-9b19-734f335aa7b3" + }, + { + "rel": "bookmark", + "href": "http://servers.api.openstack.org/1234/servers/52415800-8b69-11e0-9b19-734f335aa7b3" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/v2/1234/images/52415800-8b69-11e0-9b19-734f5736d2a2" + }, + { + "rel": "bookmark", + "href": "http://servers.api.openstack.org/1234/images/52415800-8b69-11e0-9b19-734f5736d2a2" + } + ] + } +} diff --git a/apis/openstack-nova/src/test/resources/image_list_with_block_device_mapping.json b/apis/openstack-nova/src/test/resources/image_list_with_block_device_mapping.json new file mode 100644 index 0000000000..2b80fd1a0c --- /dev/null +++ b/apis/openstack-nova/src/test/resources/image_list_with_block_device_mapping.json @@ -0,0 +1,58 @@ +{ + "images": [ + { + "status": "ACTIVE", + "updated": "2014-08-08T04:43:36Z", + "links": [ + { + "href": "http://192.168.24.16:8774/v2/d312a9d1acee46499e04fc2c0cd7e540/images/cd9d57a9-0978-45f3-9cbc-edb99347be6b", + "rel": "self" + }, + { + "href": "http://192.168.24.16:8774/d312a9d1acee46499e04fc2c0cd7e540/images/cd9d57a9-0978-45f3-9cbc-edb99347be6b", + "rel": "bookmark" + }, + { + "href": "http://192.168.24.16:9292/d312a9d1acee46499e04fc2c0cd7e540/images/cd9d57a9-0978-45f3-9cbc-edb99347be6b", + "type": "application/vnd.openstack.image", + "rel": "alternate" + } + ], + "id": "cd9d57a9-0978-45f3-9cbc-edb99347be6b", + "OS-EXT-IMG-SIZE:size": 0, + "name": "t11", + "created": "2014-08-08T04:43:36Z", + "minDisk": 0, + "progress": 100, + "minRam": 0, + "metadata": { + "block_device_mapping": [ + { + "guest_format": null, + "boot_index": 2, + "no_device": null, + "volume_id": null, + "volume_size": null, + "disk_bus": null, + "image_id": null, + "source_type": "snapshot", + "device_type": null, + "snapshot_id": "a900a56c-61b7-4438-9150-76312fa1aa10", + "destination_type": "volume", + "delete_on_termination": null + } + ], + "checksum": "32c08d302f9206668030d47789b77858", + "min_ram": "1", + "disk_format": "qcow2", + "image_name": "Ubuntu LTS 14.04", + "bdm_v2": "True", + "image_id": "cfefefc1-eba2-4b1e-9b07-a8c74a872d65", + "root_device_name": "/dev/vda", + "container_format": "bare", + "min_disk": "8", + "size": "254149120" + } + } + ] +}