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])
 

Reply via email to