Repository: libcloud Updated Branches: refs/heads/trunk 26a54f158 -> 5c09af271
Add support for Google Compute Engine Image Families Add the ability to specify an "Image Family" for creating a node and/or volume, and the ability to get an image object from a family name. Closes #778 Signed-off-by: Eric Johnson <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/5c09af27 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/5c09af27 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/5c09af27 Branch: refs/heads/trunk Commit: 5c09af2717b015c35da4d1929f1e1917892fbe1b Parents: 26a54f1 Author: Rick Wright <[email protected]> Authored: Tue Apr 26 15:07:46 2016 -0700 Committer: Eric Johnson <[email protected]> Committed: Thu Apr 28 18:28:22 2016 +0000 ---------------------------------------------------------------------- libcloud/compute/drivers/gce.py | 110 ++++++++++++++++++- .../gce/global_images_family_notfound.json | 13 +++ ...oreos-cloud_global_images_family_coreos.json | 17 +++ libcloud/test/compute/test_gce.py | 91 ++++++++++++++- 4 files changed, 221 insertions(+), 10 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/5c09af27/libcloud/compute/drivers/gce.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/gce.py b/libcloud/compute/drivers/gce.py index 4aa92ac..2538a4d 100644 --- a/libcloud/compute/drivers/gce.py +++ b/libcloud/compute/drivers/gce.py @@ -2288,7 +2288,7 @@ class GCENodeDriver(NodeDriver): description=None, ex_can_ip_forward=None, ex_disks_gce_struct=None, ex_nic_gce_struct=None, ex_on_host_maintenance=None, ex_automatic_restart=None, - ex_preemptible=None): + ex_preemptible=None, ex_image_family=None): """ Create a new node and return a node object for the node. @@ -2401,6 +2401,11 @@ class GCENodeDriver(NodeDriver): preemptible) :type ex_preemptible: ``bool`` or ``None`` + :keyword ex_image_family: Determine image from an 'Image Family' + instead of by name. 'image' should be None + to use this keyword. + :type ex_image_family: ``str`` or ``None`` + :return: A Node object for the new node. :rtype: :class:`Node` """ @@ -2408,9 +2413,15 @@ class GCENodeDriver(NodeDriver): raise ValueError("Cannot specify both 'ex_boot_disk' and " "'ex_disks_gce_struct'") - if not image and not ex_boot_disk and not ex_disks_gce_struct: + if image and ex_image_family: + raise ValueError("Cannot specify both 'image' and " + "'ex_image_family'") + + if not (image or ex_image_family or ex_boot_disk or + ex_disks_gce_struct): raise ValueError("Missing root device or image. Must specify an " - "'image', existing 'ex_boot_disk', or use the " + "'image', 'ex_image_family', existing " + "'ex_boot_disk', or use the " "'ex_disks_gce_struct'.") location = location or self.zone @@ -2420,6 +2431,8 @@ class GCENodeDriver(NodeDriver): size = self.ex_get_size(size, location) if not hasattr(ex_network, 'name'): ex_network = self.ex_get_network(ex_network) + if ex_image_family: + image = self.ex_get_image_from_family(ex_image_family) if image and not hasattr(image, 'name'): image = self.ex_get_image(image) if not hasattr(ex_disk_type, 'name'): @@ -2473,7 +2486,8 @@ class GCENodeDriver(NodeDriver): ex_disks_gce_struct=None, ex_nic_gce_struct=None, ex_on_host_maintenance=None, - ex_automatic_restart=None): + ex_automatic_restart=None, + ex_image_family=None): """ Create multiple nodes and return a list of Node objects. @@ -2599,6 +2613,11 @@ class GCENodeDriver(NodeDriver): default value for the instance type.) :type ex_automatic_restart: ``bool`` or ``None`` + :keyword ex_image_family: Determine image from an 'Image Family' + instead of by name. 'image' should be None + to use this keyword. + :type ex_image_family: ``str`` or ``None`` + :return: A list of Node objects for the new nodes. :rtype: ``list`` of :class:`Node` """ @@ -2606,6 +2625,10 @@ class GCENodeDriver(NodeDriver): raise ValueError("Cannot specify both 'image' and " "'ex_disks_gce_struct'.") + if image and ex_image_family: + raise ValueError("Cannot specify both 'image' and " + "'ex_image_family'") + location = location or self.zone if not hasattr(location, 'name'): location = self.ex_get_zone(location) @@ -2613,6 +2636,8 @@ class GCENodeDriver(NodeDriver): size = self.ex_get_size(size, location) if not hasattr(ex_network, 'name'): ex_network = self.ex_get_network(ex_network) + if ex_image_family: + image = self.ex_get_image_from_family(ex_image_family) if image and not hasattr(image, 'name'): image = self.ex_get_image(image) if not hasattr(ex_disk_type, 'name'): @@ -2842,7 +2867,7 @@ class GCENodeDriver(NodeDriver): def create_volume(self, size, name, location=None, snapshot=None, image=None, use_existing=True, - ex_disk_type='pd-standard'): + ex_disk_type='pd-standard', ex_image_family=None): """ Create a volume (disk). @@ -2872,9 +2897,21 @@ class GCENodeDriver(NodeDriver): for an SSD disk. :type ex_disk_type: ``str`` or :class:`GCEDiskType` + :keyword ex_image_family: Determine image from an 'Image Family' + instead of by name. 'image' should be None + to use this keyword. + :type ex_image_family: ``str`` or ``None`` + :return: Storage Volume object :rtype: :class:`StorageVolume` """ + if image and ex_image_family: + raise ValueError("Cannot specify both 'image' and " + "'ex_image_family'") + + if ex_image_family: + image = self.ex_get_image_from_family(ex_image_family) + request, volume_data, params = self._create_vol_req( size, name, location, snapshot, image, ex_disk_type) try: @@ -4140,6 +4177,69 @@ class GCENodeDriver(NodeDriver): partial_name), None, None) return image + def ex_get_image_from_family(self, image_family, ex_project_list=None, + ex_standard_projects=True): + """ + Return an GCENodeImage object based on an image family name. + + :param image_family: The name of the Image Family to return the + latest image from. + :type image_family: ``str`` + + :param ex_project_list: The name of the project to list for images. + Examples include: 'debian-cloud'. + :type ex_project_List: ``str``, ``list`` of ``str``, or ``None`` + + :param ex_standard_projects: If true, check in standard projects if + the image is not found. + :type ex_standard_projects: ``bool`` + + :return: GCENodeImage object based on provided information or + ResourceNotFoundError if the image family is not found. + :rtype: :class:`GCENodeImage` or raise ``ResourceNotFoundError`` + """ + def _try_image_family(image_family, project=None): + request = '/global/images/family/%s' % (image_family) + save_request_path = self.connection.request_path + if project: + new_request_path = save_request_path.replace(self.project, + project) + self.connection.request_path = new_request_path + try: + response = self.connection.request(request, method='GET') + image = self._to_node_image(response.object) + except ResourceNotFoundError: + image = None + finally: + self.connection.request_path = save_request_path + + return image + + image = None + if image_family.startswith('https://'): + response = self.connection.request(image_family, method='GET') + return self._to_node_image(response.object) + + if not ex_project_list: + image = _try_image_family(image_family) + else: + for img_proj in ex_project_list: + image = _try_image_family(image_family, project=img_proj) + if image: + break + + if not image and ex_standard_projects: + for img_proj, short_list in self.IMAGE_PROJECTS.items(): + for short_name in short_list: + if image_family.startswith(short_name): + image = _try_image_family(image_family, + project=img_proj) + + if not image: + raise ResourceNotFoundError('Could not find image for family ' + '\'%s\'' % (image_family), None, None) + return image + def ex_get_route(self, name): """ Return a Route object based on a route name. http://git-wip-us.apache.org/repos/asf/libcloud/blob/5c09af27/libcloud/test/compute/fixtures/gce/global_images_family_notfound.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/gce/global_images_family_notfound.json b/libcloud/test/compute/fixtures/gce/global_images_family_notfound.json new file mode 100644 index 0000000..5d59882 --- /dev/null +++ b/libcloud/test/compute/fixtures/gce/global_images_family_notfound.json @@ -0,0 +1,13 @@ +{ + "error": { + "code": 404, + "errors": [ + { + "domain": "global", + "message": "The resource 'projects/project-name/global/images/family/coreos' was not found", + "reason": "notFound" + } + ], + "message": "The resource 'projects/project-name/global/images/family/coreos' was not found" + } +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/5c09af27/libcloud/test/compute/fixtures/gce/projects_coreos-cloud_global_images_family_coreos.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/gce/projects_coreos-cloud_global_images_family_coreos.json b/libcloud/test/compute/fixtures/gce/projects_coreos-cloud_global_images_family_coreos.json new file mode 100644 index 0000000..8a17f15 --- /dev/null +++ b/libcloud/test/compute/fixtures/gce/projects_coreos-cloud_global_images_family_coreos.json @@ -0,0 +1,17 @@ +{ + "kind": "compute#image", + "selfLink": "https://www.googleapis.com/compute/v1/projects/coreos-cloud/global/images/coreos-beta-522-3-0-v20141226", + "id": "14171939663085407486", + "creationTimestamp": "2014-12-26T15:04:01.237-08:00", + "name": "coreos-beta-522-3-0-v20141226", + "description": "CoreOS beta 522.3.0", + "family": "coreos", + "sourceType": "RAW", + "rawDisk": { + "source": "", + "containerType": "TAR" + }, + "status": "READY", + "archiveSizeBytes": "220932284", + "diskSizeGb": "9" +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/5c09af27/libcloud/test/compute/test_gce.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/test_gce.py b/libcloud/test/compute/test_gce.py index 162a756..e8024dc 100644 --- a/libcloud/test/compute/test_gce.py +++ b/libcloud/test/compute/test_gce.py @@ -632,6 +632,18 @@ class GCENodeDriverTest(GoogleTestCase, TestCaseMixin): self.assertTrue(isinstance(node, Node)) self.assertEqual(node.name, node_name) + def test_create_node_image_family(self): + node_name = 'node-name' + size = self.driver.ex_get_size('n1-standard-1') + node = self.driver.create_node(node_name, size, image=None, + ex_image_family='coreos') + self.assertTrue(isinstance(node, Node)) + self.assertEqual(node.name, node_name) + + image = self.driver.ex_get_image('debian-7') + self.assertRaises(ValueError, self.driver.create_node, node_name, + size, image, ex_image_family='coreos') + def test_create_node_req_with_serviceaccounts(self): image = self.driver.ex_get_image('debian-7') size = self.driver.ex_get_size('n1-standard-1') @@ -770,6 +782,23 @@ class GCENodeDriverTest(GoogleTestCase, TestCaseMixin): self.assertEqual(nodes[0].name, '%s-000' % base_name) self.assertEqual(nodes[1].name, '%s-001' % base_name) + def test_ex_create_multiple_nodes_image_family(self): + base_name = 'lcnode' + image = None + size = self.driver.ex_get_size('n1-standard-1') + number = 2 + nodes = self.driver.ex_create_multiple_nodes(base_name, size, image, + number, ex_image_family='coreos') + self.assertEqual(len(nodes), 2) + self.assertTrue(isinstance(nodes[0], Node)) + self.assertTrue(isinstance(nodes[1], Node)) + self.assertEqual(nodes[0].name, '%s-000' % base_name) + self.assertEqual(nodes[1].name, '%s-001' % base_name) + + image = self.driver.ex_get_image('debian-7') + self.assertRaises(ValueError, self.driver.ex_create_multiple_nodes, + base_name, size, image, number, ex_image_family='coreos') + def test_ex_create_targethttpproxy(self): proxy_name = 'web-proxy' urlmap_name = 'web-map' @@ -822,6 +851,19 @@ class GCENodeDriverTest(GoogleTestCase, TestCaseMixin): self.assertTrue(isinstance(urlmap, GCEUrlMap)) self.assertEqual(urlmap_name, urlmap.name) + def test_create_volume_image_family(self): + volume_name = 'lcdisk' + size = 10 + volume = self.driver.create_volume(size, volume_name, + ex_image_family='coreos') + self.assertTrue(isinstance(volume, StorageVolume)) + self.assertEqual(volume.name, volume_name) + + image = self.driver.ex_get_image('debian-7') + self.assertRaises(ValueError, self.driver.create_volume, size, + volume_name, image=image, + ex_image_family='coreos') + def test_ex_create_volume_snapshot(self): snapshot_name = 'lcsnapshot' volume = self.driver.ex_get_volume('lcdisk') @@ -1167,6 +1209,29 @@ class GCENodeDriverTest(GoogleTestCase, TestCaseMixin): partial_name, 'suse-cloud', ex_standard_projects=False) + def test_ex_get_image_from_family(self): + family = 'coreos' + description = 'CoreOS beta 522.3.0' + image = self.driver.ex_get_image_from_family(family) + self.assertEqual(image.name, 'coreos-beta-522-3-0-v20141226') + self.assertEqual(image.extra['description'], description) + self.assertEqual(image.extra['family'], family) + + url = ('https://www.googleapis.com/compute/v1/projects/coreos-cloud/' + 'global/images/family/coreos') + image = self.driver.ex_get_image_from_family(url) + self.assertEqual(image.name, 'coreos-beta-522-3-0-v20141226') + self.assertEqual(image.extra['description'], description) + self.assertEqual(image.extra['family'], family) + + project_list = ['coreos-cloud'] + image = self.driver.ex_get_image_from_family(family, ex_project_list=project_list, ex_standard_projects=False) + self.assertEqual(image.name, 'coreos-beta-522-3-0-v20141226') + self.assertEqual(image.extra['description'], description) + self.assertEqual(image.extra['family'], family) + + self.assertRaises(ResourceNotFoundError, self.driver.ex_get_image_from_family, 'nofamily') + def test_ex_copy_image(self): name = 'coreos' url = 'gs://storage.core-os.net/coreos/amd64-generic/247.0.0/coreos_production_gce.tar.gz' @@ -1644,6 +1709,16 @@ class GCEMockHttp(MockHttpTestCase): body = self.fixtures.load('global_images_debian_7_wheezy_v20131014_deprecate.json') return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK]) + def _global_images_family_coreos(self, method, url, body, headers): + body = self.fixtures.load('global_images_family_notfound.json') + return (httplib.NOT_FOUND, body, self.json_hdr, + httplib.responses[httplib.NOT_FOUND]) + + def _global_images_family_nofamily(self, method, url, body, headers): + body = self.fixtures.load('global_images_family_notfound.json') + return (httplib.NOT_FOUND, body, self.json_hdr, + httplib.responses[httplib.NOT_FOUND]) + def _global_routes(self, method, url, body, headers): if method == 'POST': body = self.fixtures.load('global_routes_post.json') @@ -2098,23 +2173,29 @@ class GCEMockHttp(MockHttpTestCase): body = self.fixtures.load('projects_windows-cloud_global_images.json') return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK]) - def _projects_rhel_cloud_global_images(self, method, url, boyd, header): + def _projects_rhel_cloud_global_images(self, method, url, body, header): body = self.fixtures.load('projects_rhel-cloud_global_images.json') return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK]) - def _projects_gce_nvme_global_images(self, method, url, boyd, header): + def _projects_gce_nvme_global_images(self, method, url, body, header): body = self.fixtures.load('projects_gce-nvme_global_images.json') return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK]) - def _projects_coreos_cloud_global_images(self, method, url, boyd, header): + def _projects_coreos_cloud_global_images(self, method, url, body, header): body = self.fixtures.load('projects_coreos-cloud_global_images.json') return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK]) - def _projects_opensuse_cloud_global_images(self, method, url, boyd, header): + def _projects_coreos_cloud_global_images_family_coreos( + self, method, url, body, header): + body = self.fixtures.load( + 'projects_coreos-cloud_global_images_family_coreos.json') + return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK]) + + def _projects_opensuse_cloud_global_images(self, method, url, body, header): body = self.fixtures.load('projects_opensuse-cloud_global_images.json') return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK]) - def _projects_google_containers_global_images(self, method, url, boyd, header): + def _projects_google_containers_global_images(self, method, url, body, header): body = self.fixtures.load('projects_google-containers_global_images.json') return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
