libcloud-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From erjoh...@apache.org
Subject libcloud git commit: [google compute] Add missing attributes/methods to create_node()
Date Tue, 06 Jan 2015 13:03:33 GMT
Repository: libcloud
Updated Branches:
  refs/heads/trunk f7025a95e -> 7771e1803


[google compute] Add missing attributes/methods to create_node()

Closes #419

Signed-off-by: Eric Johnson <erjohnso@google.com>


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/7771e180
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/7771e180
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/7771e180

Branch: refs/heads/trunk
Commit: 7771e1803c0df56220b5a299cc0a2267f2ffe468
Parents: f7025a9
Author: Eric Johnson <erjohnso@google.com>
Authored: Mon Dec 15 21:21:45 2014 +0000
Committer: Eric Johnson <erjohnso@google.com>
Committed: Tue Jan 6 13:02:33 2015 +0000

----------------------------------------------------------------------
 CHANGES.rst                                     |   4 +
 demos/gce_demo.py                               |  67 ++-
 libcloud/compute/drivers/gce.py                 | 596 ++++++++++++++++---
 ...nstances_node_name_addAccessConfig_done.json |  15 +
 ...nstances_node_name_addAccessConfig_post.json |  15 +
 ...ances_node_name_deleteAccessConfig_done.json |  15 +
 ...ances_node_name_deleteAccessConfig_post.json |  15 +
 ...s_central1_a_node_name_setMetadata_post.json |  15 +
 ...1-a_instances_node_name_getSerialOutput.json |   5 +
 ..._a_instances_node_name_setMetadata_post.json |  15 +
 libcloud/test/compute/test_gce.py               | 202 ++++++-
 11 files changed, 834 insertions(+), 130 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/CHANGES.rst
----------------------------------------------------------------------
diff --git a/CHANGES.rst b/CHANGES.rst
index 80c0fd0..0b59c17 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -16,6 +16,10 @@ General
 Compute
 ~~~~~~~
 
+- Improve GCE API coverage for create_node()
+  (GITHUB-419)
+  [Eric Johnson]
+
 - GCE Licenses added to the GCE driver.
   (GITHUB-420)
   [Eric Johnson]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/demos/gce_demo.py
----------------------------------------------------------------------
diff --git a/demos/gce_demo.py b/demos/gce_demo.py
index 3535689..9e370ce 100755
--- a/demos/gce_demo.py
+++ b/demos/gce_demo.py
@@ -55,6 +55,7 @@ sys.path.append(os.path.normpath(os.path.join(os.path.dirname(__file__),
 
 from libcloud.compute.types import Provider
 from libcloud.compute.providers import get_driver
+from libcloud.common.google import ResourceNotFoundError
 
 # Maximum number of 1-CPU nodes to allow to run simultaneously
 MAX_NODES = 5
@@ -134,10 +135,16 @@ def clean_up(gce, base_name, node_list=None, resource_list=None):
     # Destroy everything else with just the destroy method
     for resource in resource_list:
         if resource.name.startswith(base_name):
-            if resource.destroy():
-                print('   Deleted %s' % resource.name)
-            else:
-                print('   Failed to Delete %s' % resource.name)
+            try:
+                resource.destroy()
+            except ResourceNotFoundError:
+                print('   Not found: %s(%s)' % (resource.name,
+                                                resource.__class__.__name__))
+            except:
+                class_name = resource.__class__.__name__
+                print('   Failed to Delete %s(%s)' % (resource.name,
+                                                      class_name))
+                raise
 
 
 # ==== DEMO CODE STARTS HERE ====
@@ -190,10 +197,44 @@ def main():
 
     # == Create Node with disk auto-created ==
     if MAX_NODES > 1:
+        print('Creating a node with multiple disks using GCE structure:')
+        name = '%s-gstruct' % DEMO_BASE_NAME
+        img_url = "projects/debian-cloud/global/images/"
+        img_url += "backports-debian-7-wheezy-v20141205"
+        disk_type_url = "projects/graphite-demos/zones/us-central1-f/"
+        disk_type_url += "diskTypes/local-ssd"
+        gce_disk_struct = [
+            {
+                "type": "PERSISTENT",
+                "deviceName": '%s-gstruct' % DEMO_BASE_NAME,
+                "initializeParams": {
+                    "diskName": '%s-gstruct' % DEMO_BASE_NAME,
+                    "sourceImage": img_url
+                },
+                "boot": True,
+                "autoDelete": True
+            },
+            {
+                "type": "SCRATCH",
+                "deviceName": '%s-gstruct-lssd' % DEMO_BASE_NAME,
+                "initializeParams": {
+                    "diskType": disk_type_url
+                },
+                "autoDelete": True
+            }
+        ]
+        node_gstruct = gce.create_node(name, 'n1-standard-1', None,
+                                       'us-central1-f',
+                                       ex_disks_gce_struct=gce_disk_struct)
+        num_disks = len(node_gstruct.extra['disks'])
+        print('    Node %s created with %d disks' % (node_gstruct.name,
+                                                     num_disks))
+
         print('Creating Node with auto-created SSD:')
         name = '%s-np-node' % DEMO_BASE_NAME
         node_1 = gce.create_node(name, 'n1-standard-1', 'debian-7',
-                                 ex_tags=['libcloud'], ex_disk_type='pd-ssd')
+                                 ex_tags=['libcloud'], ex_disk_type='pd-ssd',
+                                 ex_disk_auto_delete=False)
         print('   Node %s created' % name)
 
         # == Create, and attach a disk ==
@@ -202,6 +243,9 @@ def main():
         volume = gce.create_volume(10, disk_name)
         if volume.attach(node_1):
             print ('   Attached %s to %s' % (volume.name, node_1.name))
+        print ('   Disabled auto-delete for %s on %s' % (volume.name,
+                                                         node_1.name))
+        gce.ex_set_volume_auto_delete(volume, node_1, auto_delete=False)
 
         if CLEANUP:
             # == Detach the disk ==
@@ -233,7 +277,8 @@ def main():
     print('   Created %s from snapshot' % volume.name)
     # Create Node with Disk
     node_2 = gce.create_node(name, size, image, ex_tags=['libcloud'],
-                             ex_boot_disk=volume)
+                             ex_boot_disk=volume,
+                             ex_disk_auto_delete=False)
     print('   Node %s created with attached disk %s' % (node_2.name,
                                                         volume.name))
 
@@ -246,6 +291,13 @@ def main():
     check_node = gce.ex_get_node(node_2.name)
     print('   New tags: %s' % check_node.extra['tags'])
 
+    # == Setting Metadata for Node ==
+    print('Setting Metadata for %s' % node_2.name)
+    if gce.ex_set_node_metadata(node_2, {'foo': 'bar'}):
+        print('   Metadata updated for %s' % node_2.name)
+    check_node = gce.ex_get_node(node_2.name)
+    print('   New Metadata: %s' % check_node.extra['metadata'])
+
     # == Create Multiple nodes at once ==
     base_name = '%s-multiple-nodes' % DEMO_BASE_NAME
     number = MAX_NODES - 2
@@ -253,7 +305,8 @@ def main():
         print('Creating Multiple Nodes (%s):' % number)
         multi_nodes = gce.ex_create_multiple_nodes(base_name, size, image,
                                                    number,
-                                                   ex_tags=['libcloud'])
+                                                   ex_tags=['libcloud'],
+                                                   ex_disk_auto_delete=False)
         for node in multi_nodes:
             print('   Node %s created.' % node.name)
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/libcloud/compute/drivers/gce.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/gce.py b/libcloud/compute/drivers/gce.py
index a63c996..4e95f46 100644
--- a/libcloud/compute/drivers/gce.py
+++ b/libcloud/compute/drivers/gce.py
@@ -569,7 +569,7 @@ class GCETargetPool(UuidMixin):
 
         :param  node: Optional node to specify if only a specific node's
                       health status should be returned
-        :type   node: ``str``, ``GCENode``, or ``None``
+        :type   node: ``str``, ``Node``, or ``None``
 
         :return: List of hashes of nodes and their respective health
         :rtype:  ``list`` of ``dict``
@@ -801,6 +801,122 @@ class GCENodeDriver(NodeDriver):
         else:
             self.region = None
 
+    def ex_add_access_config(self, node, name, nat_ip=None, config_type=None):
+        """
+        Add a network interface access configuration to a node.
+
+        :keyword  node: The existing target Node (instance) that will receive
+                        the new access config.
+        :type     node: ``Node``
+
+        :keyword  name: Name of the new access config.
+        :type     node: ``str``
+
+        :keyword  nat_ip: The external existing static IP Address to use for
+                          the access config. If not provided, an ephemeral
+                          IP address will be allocated.
+        :type     nat_ip: ``str`` or ``None``
+
+        :keyword  config_type: The type of access config to create. Currently
+                               the only supported type is 'ONE_TO_ONE_NAT'.
+        :type     config_type: ``str`` or ``None``
+
+        :return: True if successful
+        :rtype:  ``bool``
+        """
+        if not isinstance(node, Node):
+            raise ValueError("Must specify a valid libcloud node object.")
+        node_name = node.name
+        zone_name = node.extra['zone'].name
+
+        config = {'name': name}
+        if config_type is None:
+            config_type = 'ONE_TO_ONE_NAT'
+        config['type'] = config_type
+
+        if nat_ip is not None:
+            config['natIP'] = nat_ip
+
+        request = '/zones/%s/instances/%s/addAccessConfig' % (zone_name,
+                                                              node_name)
+        self.connection.async_request(request, method='POST', data=config)
+        return True
+
+    def ex_delete_access_config(self, node, name, nic):
+        """
+        Delete a network interface access configuration from a node.
+
+        :keyword  node: The existing target Node (instance) for the request.
+        :type     node: ``Node``
+
+        :keyword  name: Name of the access config.
+        :type     node: ``str``
+
+        :keyword  nic: Name of the network interface.
+        :type     nic: ``str``
+
+        :return: True if successful
+        :rtype:  ``bool``
+        """
+        if not isinstance(node, Node):
+            raise ValueError("Must specify a valid libcloud node object.")
+        node_name = node.name
+        zone_name = node.extra['zone'].name
+
+        params = {'accessConfig': name, 'networkInterface': nic}
+        request = '/zones/%s/instances/%s/deleteAccessConfig' % (zone_name,
+                                                                 node_name)
+        self.connection.async_request(request, method='POST', params=params)
+        return True
+
+    def ex_set_node_metadata(self, node, metadata):
+        """
+        Set metadata for the specified node.
+
+        :keyword  node: The existing target Node (instance) for the request.
+        :type     node: ``Node``
+
+        :keyword  metadata: Set (or clear with None) metadata for this
+                            particular node.
+        :type     metadata: ``dict`` or ``None``
+
+        :return: True if successful
+        :rtype:  ``bool``
+        """
+        if not isinstance(node, Node):
+            raise ValueError("Must specify a valid libcloud node object.")
+        node_name = node.name
+        zone_name = node.extra['zone'].name
+        if 'metadata' in node.extra and \
+                'fingerprint' in node.extra['metadata']:
+            current_fp = node.extra['metadata']['fingerprint']
+        else:
+            current_fp = 'absent'
+        body = self._format_metadata(current_fp, metadata)
+        request = '/zones/%s/instances/%s/setMetadata' % (zone_name,
+                                                          node_name)
+        self.connection.async_request(request, method='POST', data=body)
+        return True
+
+    def ex_get_serial_output(self, node):
+        """
+        Fetch the console/serial port output from the node.
+
+        :keyword  node: The existing target Node (instance) for the request.
+        :type     node: ``Node``
+
+        :return: A string containing serial port output of the node.
+        :rtype:  ``str``
+        """
+        if not isinstance(node, Node):
+            raise ValueError("Must specify a valid libcloud node object.")
+        node_name = node.name
+        zone_name = node.extra['zone'].name
+        request = '/zones/%s/instances/%s/serialPort' % (zone_name,
+                                                         node_name)
+        response = self.connection.request(request, method='GET').object
+        return response['contents']
+
     def ex_list_disktypes(self, zone=None):
         """
         Return a list of DiskTypes for a zone or all.
@@ -890,16 +1006,7 @@ class GCENodeDriver(NodeDriver):
         :rtype:  ``bool``
         """
         if metadata:
-            if not isinstance(metadata, dict):
-                raise ValueError("Metadata must be a python dictionary.")
-
-            if 'items' not in metadata:
-                items = []
-                for k, v in metadata.items():
-                    items.append({'key': k, 'value': v})
-                metadata = {'items': items}
-            elif not isinstance(metadata['items'], list):
-                raise ValueError("Invalid GCE metadata format.")
+            metadata = self._format_metadata('na', metadata)
 
         request = '/setCommonInstanceMetadata'
 
@@ -1606,7 +1713,7 @@ class GCENodeDriver(NodeDriver):
         :param  next_hop: Next traffic hop. Use ``None`` for the default
                           Internet gateway, or specify an instance or IP
                           address.
-        :type   next_hop: ``str``, ``GCENode``, or ``None``
+        :type   next_hop: ``str``, ``Node``, or ``None``
 
         :param  description: Custom description for the route.
         :type   description: ``str`` or ``None``
@@ -1677,7 +1784,10 @@ class GCENodeDriver(NodeDriver):
                     ex_network='default', ex_tags=None, ex_metadata=None,
                     ex_boot_disk=None, use_existing_disk=True,
                     external_ip='ephemeral', ex_disk_type='pd-standard',
-                    ex_disk_auto_delete=True, ex_service_accounts=None):
+                    ex_disk_auto_delete=True, ex_service_accounts=None,
+                    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):
         """
         Create a new node and return a node object for the node.
 
@@ -1689,7 +1799,7 @@ class GCENodeDriver(NodeDriver):
 
         :param  image: The image to use to create the node (or, if attaching
                        a persistent disk, the image used to create the disk)
-        :type   image: ``str`` or :class:`GCENodeImage`
+        :type   image: ``str`` or :class:`GCENodeImage` or ``None``
 
         :keyword  location: The location (zone) to create the node in.
         :type     location: ``str`` or :class:`NodeLocation` or
@@ -1742,9 +1852,56 @@ class GCENodeDriver(NodeDriver):
                                        'gcloud compute'.
         :type     ex_service_accounts: ``list``
 
+        :keyword  description: The description of the node (instance).
+        :type     description: ``str`` or ``None``
+
+        :keyword  ex_can_ip_forward: Set to ``True`` to allow this node to
+                                  send/receive non-matching src/dst packets.
+        :type     ex_can_ip_forward: ``bool`` or ``None``
+
+        :keyword  ex_disks_gce_struct: Support for passing in the GCE-specific
+                                       formatted disks[] structure. No attempt
+                                       is made to ensure proper formatting of
+                                       the disks[] structure. Using this
+                                       structure obviates the need of using
+                                       other disk params like 'ex_boot_disk',
+                                       etc. See the GCE docs for specific
+                                       details.
+        :type     ex_disks_gce_struct: ``list`` or ``None``
+
+        :keyword  ex_nic_gce_struct: Support passing in the GCE-specific
+                                     formatted networkInterfaces[] structure.
+                                     No attempt is made to ensure proper
+                                     formatting of the networkInterfaces[]
+                                     data. Using this structure obviates the
+                                     need of using 'external_ip' and
+                                     'ex_network'.  See the GCE docs for
+                                     details.
+        :type     ex_nic_gce_struct: ``list`` or ``None``
+n
+        :keyword  ex_on_host_maintenance: Defines whether node should be
+                                          terminated or migrated when host
+                                          machine goes down. Acceptable values
+                                          are: 'MIGRATE' or 'TERMINATE' (If
+                                          not supplied, value will be reset to
+                                          GCE default value for the instance
+                                          type.)
+        :type     ex_on_host_maintenance: ``str`` or ``None``
+
+        :keyword  ex_automatic_restart: Defines whether the instance should be
+                                        automatically restarted when it is
+                                        terminated by Compute Engine. (If not
+                                        supplied, value will be set to the GCE
+                                        default value for the instance type.)
+        :type     ex_automatic_restart: ``bool`` or ``None``
+
         :return:  A Node object for the new node.
         :rtype:   :class:`Node`
         """
+        if ex_boot_disk and ex_disks_gce_struct:
+            raise ValueError("Cannot specify both 'ex_boot_disk' and "
+                             "'ex_disks_gce_struct'")
+
         location = location or self.zone
         if not hasattr(location, 'name'):
             location = self.ex_get_zone(location)
@@ -1752,32 +1909,22 @@ 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 not hasattr(image, 'name'):
+        if image and not hasattr(image, 'name'):
             image = self.ex_get_image(image)
 
-        if not ex_boot_disk:
-            ex_boot_disk = self.create_volume(None, name, location=location,
-                                              image=image,
-                                              use_existing=use_existing_disk,
-                                              ex_disk_type=ex_disk_type)
-
-        if not ex_metadata:
-            ex_metadata = None
-        elif not isinstance(ex_metadata, dict):
-            raise ValueError('metadata field is not a dictionnary.')
-        else:
-            if 'items' not in ex_metadata:
-                # The expected GCE format is odd:
-                # items: [{'value': '1', 'key': 'one'},
-                #        {'value': '2', 'key': 'two'},
-                #        {'value': 'N', 'key': 'N'}]
-                # So the only real key is items, and the values are tuples
-                # Since arbitrary values are fine, we only check for the key.
-                # If missing, we prefix it to the items.
-                items = []
-                for k, v in ex_metadata.items():
-                    items.append({'key': k, 'value': v})
-                ex_metadata = {'items': items}
+        # Use disks[].initializeParams to auto-create the boot disk
+        if not ex_disks_gce_struct and not ex_boot_disk:
+            ex_disks_gce_struct = [{
+                'autoDelete': ex_disk_auto_delete,
+                'boot': True,
+                'type': 'PERSISTENT',
+                'mode': 'READ_WRITE',
+                'deviceName': name,
+                'initializeParams': {
+                    'diskName': name,
+                    'sourceImage': image.extra['selfLink']
+                }
+            }]
 
         request, node_data = self._create_node_req(name, size, image,
                                                    location, ex_network,
@@ -1785,9 +1932,14 @@ class GCENodeDriver(NodeDriver):
                                                    ex_boot_disk, external_ip,
                                                    ex_disk_type,
                                                    ex_disk_auto_delete,
-                                                   ex_service_accounts)
+                                                   ex_service_accounts,
+                                                   description,
+                                                   ex_can_ip_forward,
+                                                   ex_disks_gce_struct,
+                                                   ex_nic_gce_struct,
+                                                   ex_on_host_maintenance,
+                                                   ex_automatic_restart)
         self.connection.async_request(request, method='POST', data=node_data)
-
         return self.ex_get_node(name, location.name)
 
     def ex_create_multiple_nodes(self, base_name, size, image, number,
@@ -1796,9 +1948,15 @@ class GCENodeDriver(NodeDriver):
                                  ignore_errors=True, use_existing_disk=True,
                                  poll_interval=2, external_ip='ephemeral',
                                  ex_disk_type='pd-standard',
-                                 ex_auto_disk_delete=True,
+                                 ex_disk_auto_delete=True,
                                  ex_service_accounts=None,
-                                 timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
+                                 timeout=DEFAULT_TASK_COMPLETION_TIMEOUT,
+                                 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):
         """
         Create multiple nodes and return a list of Node objects.
 
@@ -1881,6 +2039,49 @@ class GCENodeDriver(NodeDriver):
                            created before timing out.
         :type     timeout: ``int``
 
+        :keyword  description: The description of the node (instance).
+        :type     description: ``str`` or ``None``
+
+        :keyword  ex_can_ip_forward: Set to ``True`` to allow this node to
+                                  send/receive non-matching src/dst packets.
+        :type     ex_can_ip_forward: ``bool`` or ``None``
+
+        :keyword  ex_disks_gce_struct: Support for passing in the GCE-specific
+                                       formatted disks[] structure. No attempt
+                                       is made to ensure proper formatting of
+                                       the disks[] structure. Using this
+                                       structure obviates the need of using
+                                       other disk params like 'ex_boot_disk',
+                                       etc. See the GCE docs for specific
+                                       details.
+        :type     ex_disks_gce_struct: ``list`` or ``None``
+
+        :keyword  ex_nic_gce_struct: Support passing in the GCE-specific
+                                     formatted networkInterfaces[] structure.
+                                     No attempt is made to ensure proper
+                                     formatting of the networkInterfaces[]
+                                     data. Using this structure obviates the
+                                     need of using 'external_ip' and
+                                     'ex_network'.  See the GCE docs for
+                                     details.
+        :type     ex_nic_gce_struct: ``list`` or ``None``
+n
+        :keyword  ex_on_host_maintenance: Defines whether node should be
+                                          terminated or migrated when host
+                                          machine goes down. Acceptable values
+                                          are: 'MIGRATE' or 'TERMINATE' (If
+                                          not supplied, value will be reset to
+                                          GCE default value for the instance
+                                          type.)
+        :type     ex_on_host_maintenance: ``str`` or ``None``
+
+        :keyword  ex_automatic_restart: Defines whether the instance should be
+                                        automatically restarted when it is
+                                        terminated by Compute Engine. (If not
+                                        supplied, value will be set to the GCE
+                                        default value for the instance type.)
+        :type     ex_automatic_restart: ``bool`` or ``None``
+
         :return:  A list of Node objects for the new nodes.
         :rtype:   ``list`` of :class:`Node`
         """
@@ -1904,7 +2105,13 @@ class GCENodeDriver(NodeDriver):
                       'use_existing_disk': use_existing_disk,
                       'external_ip': external_ip,
                       'ex_disk_type': ex_disk_type,
-                      'ex_service_accounts': ex_service_accounts}
+                      'ex_service_accounts': ex_service_accounts,
+                      'description': description,
+                      'ex_can_ip_forward': ex_can_ip_forward,
+                      'ex_disks_gce_struct': ex_disks_gce_struct,
+                      'ex_nic_gce_struct': ex_nic_gce_struct,
+                      'ex_on_host_maintenance': ex_on_host_maintenance,
+                      'ex_automatic_restart': ex_automatic_restart}
 
         # List for holding the status information for disk/node creation.
         status_list = []
@@ -2234,10 +2441,10 @@ class GCENodeDriver(NodeDriver):
 
         :param  node: Optional node to specify if only a specific node's
                       health status should be returned
-        :type   node: ``str``, ``GCENode``, or ``None``
+        :type   node: ``str``, ``Node``, or ``None``
 
         :return: List of hashes of instances and their respective health,
-                 e.g. [{'node': ``GCENode``, 'health': 'UNHEALTHY'}, ...]
+                 e.g. [{'node': ``Node``, 'health': 'UNHEALTHY'}, ...]
         :rtype:  ``list`` of ``dict``
         """
         health = []
@@ -2596,6 +2803,7 @@ class GCENodeDriver(NodeDriver):
         """
         with open(script, 'r') as f:
             script_data = f.read()
+        # TODO(erjohnso): allow user defined metadata here...
         metadata = {'items': [{'key': 'startup-script',
                                'value': script_data}]}
 
@@ -2605,17 +2813,18 @@ class GCENodeDriver(NodeDriver):
                                 ex_service_accounts=ex_service_accounts)
 
     def attach_volume(self, node, volume, device=None, ex_mode=None,
-                      ex_boot=False):
+                      ex_boot=False, ex_type=None, ex_source=None,
+                      ex_auto_delete=None, ex_initialize_params=None,
+                      ex_licenses=None, ex_interface=None):
         """
         Attach a volume to a node.
 
-        If volume is None, a scratch disk will be created and attached.
+        If volume is None, an ex_source URL must be provided.
 
         :param  node: The node to attach the volume to
-        :type   node: :class:`Node`
+        :type   node: :class:`Node` or ``None``
 
-        :param  volume: The volume to attach. If none, a scratch disk will be
-                        attached.
+        :param  volume: The volume to attach.
         :type   volume: :class:`StorageVolume` or ``None``
 
         :keyword  device: The device name to attach the volume as. Defaults to
@@ -2628,16 +2837,53 @@ class GCENodeDriver(NodeDriver):
         :keyword  ex_boot: If true, disk will be attached as a boot disk
         :type     ex_boot: ``bool``
 
+        :keyword  ex_type: Specify either 'PERSISTENT' (default) or 'SCRATCH'.
+        :type     ex_type: ``str``
+
+        :keyword  ex_source: URL (full or partial) of disk source. Must be
+                             present if not using an existing StorageVolume.
+        :type     ex_source: ``str`` or ``None``
+
+        :keyword  ex_auto_delete: If set, the disk will be auto-deleted
+                                  if the parent node/instance is deleted.
+        :type     ex_auto_delete: ``bool`` or ``None``
+
+        :keyword  ex_initialize_params: Allow user to pass in full JSON
+                                        struct of `initializeParams` as
+                                        documented in GCE's API.
+        :type     ex_initialize_params: ``dict`` or ``None``
+
+        :keyword  ex_licenses: List of strings representing licenses
+                               associated with the volume/disk.
+        :type     ex_licenses: ``list`` of ``str``
+
+        :keyword  ex_interface: User can specify either 'SCSI' (default) or
+                                'NVME'.
+        :type     ex_interface: ``str`` or ``None``
+
         :return:  True if successful
         :rtype:   ``bool``
         """
+        if volume is None and ex_source is None:
+            raise ValueError("Must supply either a StorageVolume or "
+                             "set `ex_source` URL for an existing disk.")
+        if volume is None and device is None:
+            raise ValueError("Must supply either a StorageVolume or "
+                             "set `device` name.")
+
         volume_data = {}
-        if volume is None:
-            volume_data['type'] = 'SCRATCH'
-        else:
-            volume_data['type'] = 'PERSISTENT'
-            volume_data['source'] = volume.extra['selfLink']
-        volume_data['kind'] = 'compute#attachedDisk'
+        if ex_source:
+            volume_data['source'] = ex_source
+        if ex_initialize_params:
+            volume_data['initialzeParams'] = ex_initialize_params
+        if ex_licenses:
+            volume_data['licenses'] = ex_licenses
+        if ex_interface:
+            volume_data['interface'] = ex_interface
+        if ex_type:
+            volume_data['type'] = ex_type
+
+        volume_data['source'] = ex_source or volume.extra['selfLink']
         volume_data['mode'] = ex_mode or 'READ_WRITE'
 
         if device:
@@ -2695,7 +2941,7 @@ class GCENodeDriver(NodeDriver):
             node.extra['zone'].name, node.name
         )
         delete_params = {
-            'deviceName': volume,
+            'deviceName': volume.name,
             'autoDelete': auto_delete,
         }
         self.connection.async_request(request, method='POST',
@@ -2787,26 +3033,17 @@ class GCENodeDriver(NodeDriver):
             'replacement': replacement.extra['selfLink'],
         }
 
-        if deprecated is not None:
-            try:
-                _ = timestamp_to_datetime(deprecated)    # NOQA
-            except:
-                raise ValueError('deprecated must be an RFC3339 timestamp')
-            image_data['deprecated'] = deprecated
-
-        if obsolete is not None:
-            try:
-                _ = timestamp_to_datetime(obsolete)      # NOQA
-            except:
-                raise ValueError('obsolete must be an RFC3339 timestamp')
-            image_data['obsolete'] = obsolete
+        for attribute, value in [('deprecated', deprecated),
+                                 ('obsolete', obsolete),
+                                 ('deleted', deleted)]:
+            if value is None:
+                continue
 
-        if deleted is not None:
             try:
-                _ = timestamp_to_datetime(deleted)       # NOQA
+                timestamp_to_datetime(value)
             except:
-                raise ValueError('deleted must be an RFC3339 timestamp')
-            image_data['deleted'] = deleted
+                raise ValueError('%s must be an RFC3339 timestamp' % attribute)
+            image_data[attribute] = value
 
         request = '/global/images/%s/deprecate' % (image.name)
 
@@ -3644,10 +3881,14 @@ class GCENodeDriver(NodeDriver):
             zone = self.ex_get_zone(zone)
         return zone
 
-    def _create_node_req(self, name, size, image, location, network,
+    def _create_node_req(self, name, size, image, location, network=None,
                          tags=None, metadata=None, boot_disk=None,
                          external_ip='ephemeral', ex_disk_type='pd-standard',
-                         ex_disk_auto_delete=True, ex_service_accounts=None):
+                         ex_disk_auto_delete=True, ex_service_accounts=None,
+                         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):
         """
         Returns a request and body to create a new node.  This is a helper
         method to support both :class:`create_node` and
@@ -3661,7 +3902,7 @@ class GCENodeDriver(NodeDriver):
 
         :param  image: The image to use to create the node (or, if using a
                        persistent disk, the image the disk was created from).
-        :type   image: :class:`GCENodeImage`
+        :type   image: :class:`GCENodeImage` or ``None``
 
         :param  location: The location (zone) to create the node in.
         :type   location: :class:`NodeLocation` or :class:`GCEZone`
@@ -3675,8 +3916,8 @@ class GCENodeDriver(NodeDriver):
         :keyword  metadata: Metadata dictionary for instance.
         :type     metadata: ``dict``
 
-        :keyword  boot_disk:  Persistent boot disk to attach.
-        :type     :class:`StorageVolume`
+        :keyword  boot_disk: Persistent boot disk to attach.
+        :type     :class:`StorageVolume` or ``None``
 
         :keyword  external_ip: The external IP address to use.  If 'ephemeral'
                                (default), a new non-static address will be
@@ -3708,6 +3949,49 @@ class GCENodeDriver(NodeDriver):
                                        'gcloud compute'.
         :type     ex_service_accounts: ``list``
 
+        :keyword  description: The description of the node (instance).
+        :type     description: ``str`` or ``None``
+
+        :keyword  ex_can_ip_forward: Set to ``True`` to allow this node to
+                                  send/receive non-matching src/dst packets.
+        :type     ex_can_ip_forward: ``bool`` or ``None``
+
+        :keyword  ex_disks_gce_struct: Support for passing in the GCE-specific
+                                       formatted disks[] structure. No attempt
+                                       is made to ensure proper formatting of
+                                       the disks[] structure. Using this
+                                       structure obviates the need of using
+                                       other disk params like 'ex_boot_disk',
+                                       etc. See the GCE docs for specific
+                                       details.
+        :type     ex_disks_gce_struct: ``list`` or ``None``
+
+        :keyword  ex_nic_gce_struct: Support passing in the GCE-specific
+                                     formatted networkInterfaces[] structure.
+                                     No attempt is made to ensure proper
+                                     formatting of the networkInterfaces[]
+                                     data. Using this structure obviates the
+                                     need of using 'external_ip' and
+                                     'ex_network'.  See the GCE docs for
+                                     details.
+        :type     ex_nic_gce_struct: ``list`` or ``None``
+n
+        :keyword  ex_on_host_maintenance: Defines whether node should be
+                                          terminated or migrated when host
+                                          machine goes down. Acceptable values
+                                          are: 'MIGRATE' or 'TERMINATE' (If
+                                          not supplied, value will be reset to
+                                          GCE default value for the instance
+                                          type.)
+        :type     ex_on_host_maintenance: ``str`` or ``None``
+
+        :keyword  ex_automatic_restart: Defines whether the instance should be
+                                        automatically restarted when it is
+                                        terminated by Compute Engine. (If not
+                                        supplied, value will be set to the GCE
+                                        default value for the instance type.)
+        :type     ex_automatic_restart: ``bool`` or ``None``
+
         :return:  A tuple containing a request string and a node_data dict.
         :rtype:   ``tuple`` of ``str`` and ``dict``
         """
@@ -3717,7 +4001,8 @@ class GCENodeDriver(NodeDriver):
         if tags:
             node_data['tags'] = {'items': tags}
         if metadata:
-            node_data['metadata'] = metadata
+            node_data['metadata'] = self._format_metadata(fingerprint='na',
+                                                          metadata=metadata)
 
         # by default, new instances will match the same serviceAccount and
         # scope set in the Developers Console and Cloud SDK
@@ -3751,11 +4036,14 @@ class GCENodeDriver(NodeDriver):
                 set_scopes.append(sa)
         node_data['serviceAccounts'] = set_scopes
 
+        if boot_disk and ex_disks_gce_struct:
+            raise ValueError("Cannot specify both 'boot_disk' and "
+                             "'ex_disks_gce_struct'. Use one or the other.")
+
         if boot_disk:
             if not isinstance(ex_disk_auto_delete, bool):
                 raise ValueError("ex_disk_auto_delete field is not a bool.")
-            disks = [{'kind': 'compute#attachedDisk',
-                      'boot': True,
+            disks = [{'boot': True,
                       'type': 'PERSISTENT',
                       'mode': 'READ_WRITE',
                       'deviceName': boot_disk.name,
@@ -3763,21 +4051,44 @@ class GCENodeDriver(NodeDriver):
                       'zone': boot_disk.extra['zone'].extra['selfLink'],
                       'source': boot_disk.extra['selfLink']}]
             node_data['disks'] = disks
+
+        if ex_disks_gce_struct:
+            node_data['disks'] = ex_disks_gce_struct
+
+        if network and ex_nic_gce_struct:
+            raise ValueError("Cannot specify both 'network' and "
+                             "'ex_nic_gce_struct'. Use one or the other.")
+
+        if network:
+            ni = [{'kind': 'compute#instanceNetworkInterface',
+                   'network': network.extra['selfLink']}]
+            if external_ip:
+                access_configs = [{'name': 'External NAT',
+                                   'type': 'ONE_TO_ONE_NAT'}]
+                if hasattr(external_ip, 'address'):
+                    access_configs[0]['natIP'] = external_ip.address
+                ni[0]['accessConfigs'] = access_configs
         else:
-            node_data['image'] = image.extra['selfLink']
-
-        ni = [{'kind': 'compute#instanceNetworkInterface',
-               'network': network.extra['selfLink']}]
-        if external_ip:
-            access_configs = [{'name': 'External NAT',
-                               'type': 'ONE_TO_ONE_NAT'}]
-            if hasattr(external_ip, 'address'):
-                access_configs[0]['natIP'] = external_ip.address
-            ni[0]['accessConfigs'] = access_configs
+            ni = ex_nic_gce_struct
         node_data['networkInterfaces'] = ni
 
-        request = '/zones/%s/instances' % (location.name)
+        if description:
+            node_data['description'] = str(description)
+        if ex_can_ip_forward:
+            node_data['canIpForward'] = True
+        scheduling = {}
+        if ex_on_host_maintenance:
+            if isinstance(ex_on_host_maintenance, str) and \
+                    ex_on_host_maintenance in ['MIGRATE', 'TERMINATE']:
+                scheduling['onHostMaintenance'] = ex_on_host_maintenance
+            else:
+                scheduling['onHostMaintenance'] = 'MIGRATE'
+        if ex_automatic_restart is not None:
+            scheduling['automaticRestart'] = ex_automatic_restart
+        if scheduling:
+            node_data['scheduling'] = scheduling
 
+        request = '/zones/%s/instances' % (location.name)
         return request, node_data
 
     def _multi_create_disk(self, status, node_attrs):
@@ -3874,7 +4185,14 @@ class GCENodeDriver(NodeDriver):
             node_attrs['location'], node_attrs['network'], node_attrs['tags'],
             node_attrs['metadata'], boot_disk=status['disk'],
             external_ip=node_attrs['external_ip'],
-            ex_service_accounts=node_attrs['ex_service_accounts'])
+            ex_service_accounts=node_attrs['ex_service_accounts'],
+            description=node_attrs['description'],
+            ex_can_ip_forward=node_attrs['ex_can_ip_forward'],
+            ex_disks_gce_struct=node_attrs['ex_disks_gce_struct'],
+            ex_nic_gce_struct=node_attrs['ex_nic_gce_struct'],
+            ex_on_host_maintenance=node_attrs['ex_on_host_maintenance'],
+            ex_automatic_restart=node_attrs['ex_automatic_restart'])
+
         try:
             node_res = self.connection.request(
                 request, method='POST', data=node_data).object
@@ -4231,6 +4549,7 @@ class GCENodeDriver(NodeDriver):
         extra = {}
 
         extra['status'] = node.get('status')
+        extra['statusMessage'] = node.get('statusMessage')
         extra['description'] = node.get('description')
         extra['zone'] = self.ex_get_zone(node['zone'])
         extra['image'] = node.get('image')
@@ -4239,11 +4558,16 @@ class GCENodeDriver(NodeDriver):
         extra['networkInterfaces'] = node.get('networkInterfaces')
         extra['id'] = node['id']
         extra['selfLink'] = node.get('selfLink')
+        extra['kind'] = node.get('kind')
+        extra['creationTimestamp'] = node.get('creationTimestamp')
         extra['name'] = node['name']
         extra['metadata'] = node.get('metadata', {})
         extra['tags_fingerprint'] = node['tags']['fingerprint']
         extra['scheduling'] = node.get('scheduling', {})
         extra['deprecated'] = True if node.get('deprecated', None) else False
+        extra['canIpForward'] = node.get('canIpForward')
+        extra['serviceAccounts'] = node.get('serviceAccounts', [])
+        extra['scheduling'] = node.get('scheduling', {})
 
         for disk in extra['disks']:
             if disk.get('boot') and disk.get('type') == 'PERSISTENT':
@@ -4471,6 +4795,90 @@ class GCENodeDriver(NodeDriver):
                              region=region, healthchecks=healthcheck_list,
                              nodes=node_list, driver=self, extra=extra)
 
+    def _format_metadata(self, fingerprint, metadata=None):
+        """
+        Convert various data formats into the metadata format expected by
+        Google Compute Engine and suitable for passing along to the API. Can
+        accept the following formats:
+
+          (a) [{'key': 'k1', 'value': 'v1'}, ...]
+          (b) [{'k1': 'v1'}, ...]
+          (c) {'key': 'k1', 'value': 'v1'}
+          (d) {'k1': 'v1', 'k2': v2', ...}
+          (e) {'items': [...]}       # does not check for valid list contents
+
+        The return value is a 'dict' that GCE expects, e.g.
+
+          {'fingerprint': 'xx...',
+           'items': [{'key': 'key1', 'value': 'val1'},
+                     {'key': 'key2', 'value': 'val2'},
+                     ...,
+                    ]
+          }
+
+        :param  fingerprint: Current metadata fingerprint
+        :type   fingerprint: ``str``
+
+        :param  metadata: Variety of input formats.
+        :type   metadata: ``list``, ``dict``, or ``None``
+
+        :return: GCE-friendly metadata dict
+        :rtype:  ``dict``
+        """
+        if not metadata:
+            return {'fingerprint': fingerprint, 'items': []}
+        md = {'fingerprint': fingerprint}
+
+        # Check `list` format. Can support / convert the following:
+        # (a) [{'key': 'k1', 'value': 'v1'}, ...]
+        # (b) [{'k1': 'v1'}, ...]
+        if isinstance(metadata, list):
+            item_list = []
+            for i in metadata:
+                if isinstance(i, dict):
+                    # check (a)
+                    if 'key' in i and 'value' in i and len(i) == 2:
+                        item_list.append(i)
+                    # check (b)
+                    elif len(i) == 1:
+                        item_list.append({'key': list(i.keys())[0],
+                                          'value': list(i.values())[0]})
+                    else:
+                        raise ValueError("Unsupported metadata format.")
+                else:
+                    raise ValueError("Unsupported metadata format.")
+            md['items'] = item_list
+
+        # Check `dict` format. Can support / convert the following:
+        # (c) {'key': 'k1', 'value': 'v1'}
+        # (d) {'k1': 'v1', 'k2': 'v2', ...}
+        # (e) {'items': [...]}
+        if isinstance(metadata, dict):
+            # Check (c)
+            if 'key' in metadata and 'value' in metadata and \
+                    len(metadata) == 2:
+                md['items'] = [metadata]
+            # check (d)
+            elif len(metadata) == 1:
+                if 'items' in metadata:
+                    # check (e)
+                    if isinstance(metadata['items'], list):
+                        md['items'] = metadata['items']
+                    else:
+                        raise ValueError("Unsupported metadata format.")
+                else:
+                    md['items'] = [{'key': list(metadata.keys())[0],
+                                   'value': list(metadata.values())[0]}]
+            else:
+                # check (d)
+                md['items'] = []
+                for k, v in metadata.items():
+                    md['items'].append({'key': k, 'value': v})
+
+        if 'items' not in md:
+            raise ValueError("Unsupported metadata format.")
+        return md
+
     def _to_zone(self, zone):
         """
         Return a Zone object from the json-response dictionary.

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_done.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_done.json b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_done.json
new file mode 100644
index 0000000..72dcd1a
--- /dev/null
+++ b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_done.json
@@ -0,0 +1,15 @@
+{
+  "endTime": "2013-06-26T16:13:08.382-07:00",
+  "id": "1858155812259649243",
+  "insertTime": "2013-06-26T16:12:51.492-07:00",
+  "kind": "compute#operation",
+  "name": "operation-zones_us-central1-a_instances_node_name_addAccessConfig_post",
+  "operationType": "insert",
+  "progress": 100,
+  "selfLink": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a/operations/operation-zones_us-central1-a_instances_node_name_addAccessConfig_post",
+  "startTime": "2013-06-26T16:12:51.537-07:00",
+  "status": "DONE",
+  "targetId": "16630486471904253898",
+  "user": "foo@developer.gserviceaccount.com",
+  "zone": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a"
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_post.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_post.json b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_post.json
new file mode 100644
index 0000000..fc806f4
--- /dev/null
+++ b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_post.json
@@ -0,0 +1,15 @@
+{
+  "endTime": "2013-06-26T16:13:08.382-07:00",
+  "id": "1858155812259649243",
+  "insertTime": "2013-06-26T16:12:51.492-07:00",
+  "kind": "compute#operation",
+  "name": "operation-zones_us-central1-a_instances_node_name_addAccessConfig_post",
+  "operationType": "insert",
+  "progress": 0,
+  "selfLink": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a/operations/operation-zones_us-central1-a_instances_node_name_addAccessConfig_post",
+  "startTime": "2013-06-26T16:12:51.537-07:00",
+  "status": "PENDING",
+  "targetId": "16630486471904253898",
+  "user": "foo@developer.gserviceaccount.com",
+  "zone": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a"
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_done.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_done.json b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_done.json
new file mode 100644
index 0000000..9f15369
--- /dev/null
+++ b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_done.json
@@ -0,0 +1,15 @@
+{
+  "endTime": "2013-06-26T16:13:08.382-07:00",
+  "id": "1858155812259649243",
+  "insertTime": "2013-06-26T16:12:51.492-07:00",
+  "kind": "compute#operation",
+  "name": "operation-zones_us-central1-a_instances_node_name_deleteAccessConfig_post",
+  "operationType": "delete",
+  "progress": 100,
+  "selfLink": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a/operations/operation-zones_us-central1-a_instances_node_name_deleteAccessConfig_post",
+  "startTime": "2013-06-26T16:12:51.537-07:00",
+  "status": "DONE",
+  "targetId": "16630486471904253898",
+  "user": "foo@developer.gserviceaccount.com",
+  "zone": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a"
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_post.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_post.json b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_post.json
new file mode 100644
index 0000000..b41d85c
--- /dev/null
+++ b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_post.json
@@ -0,0 +1,15 @@
+{
+  "endTime": "2013-06-26T16:13:08.382-07:00",
+  "id": "1858155812259649243",
+  "insertTime": "2013-06-26T16:12:51.492-07:00",
+  "kind": "compute#operation",
+  "name": "operation-zones_us-central1-a_instances_node_name_deleteAccessConfig_post",
+  "operationType": "delete",
+  "progress": 0,
+  "selfLink": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a/operations/operation-zones_us-central1-a_instances_node_name_deleteAccessConfig_post",
+  "startTime": "2013-06-26T16:12:51.537-07:00",
+  "status": "PENDING",
+  "targetId": "16630486471904253898",
+  "user": "foo@developer.gserviceaccount.com",
+  "zone": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a"
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/libcloud/test/compute/fixtures/gce/operations_operation_zones_us_central1_a_node_name_setMetadata_post.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/gce/operations_operation_zones_us_central1_a_node_name_setMetadata_post.json b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us_central1_a_node_name_setMetadata_post.json
new file mode 100644
index 0000000..c1d8060
--- /dev/null
+++ b/libcloud/test/compute/fixtures/gce/operations_operation_zones_us_central1_a_node_name_setMetadata_post.json
@@ -0,0 +1,15 @@
+{
+  "endTime": "2013-06-26T10:05:07.630-07:00",
+  "id": "3681664092089171723",
+  "insertTime": "2013-06-26T10:05:03.271-07:00",
+  "kind": "compute#operation",
+  "name": "operation-setMetadata_post",
+  "operationType": "insert",
+  "progress": 100,
+  "selfLink": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a/operations/operation-setMetadata_post",
+  "startTime": "2013-06-26T10:05:03.315-07:00",
+  "status": "DONE",
+  "targetId": "16211908079305042870",
+  "targetLink": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a/instances/node-name/setMetadata",
+  "user": "foo@developer.gserviceaccount.com"
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/libcloud/test/compute/fixtures/gce/zones_us-central1-a_instances_node_name_getSerialOutput.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/gce/zones_us-central1-a_instances_node_name_getSerialOutput.json b/libcloud/test/compute/fixtures/gce/zones_us-central1-a_instances_node_name_getSerialOutput.json
new file mode 100644
index 0000000..4b280b3
--- /dev/null
+++ b/libcloud/test/compute/fixtures/gce/zones_us-central1-a_instances_node_name_getSerialOutput.json
@@ -0,0 +1,5 @@
+{
+ "kind": "compute#serialPortOutput",
+ "selfLink": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a/instances/node-name/serialPort",
+ "contents": "This is some serial\r\noutput for you."
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/libcloud/test/compute/fixtures/gce/zones_us_central1_a_instances_node_name_setMetadata_post.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/gce/zones_us_central1_a_instances_node_name_setMetadata_post.json b/libcloud/test/compute/fixtures/gce/zones_us_central1_a_instances_node_name_setMetadata_post.json
new file mode 100644
index 0000000..d2ef984
--- /dev/null
+++ b/libcloud/test/compute/fixtures/gce/zones_us_central1_a_instances_node_name_setMetadata_post.json
@@ -0,0 +1,15 @@
+{
+  "endTime": "2013-06-26T10:05:07.630-07:00",
+  "id": "3681664092089171723",
+  "insertTime": "2013-06-26T10:05:03.271-07:00",
+  "kind": "compute#operation",
+  "name": "operation-setMetadata_post",
+  "operationType": "insert",
+  "progress": 0,
+  "selfLink": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a/operations/operation-setMetadata_post",
+  "startTime": "2013-06-26T10:05:03.315-07:00",
+  "status": "PENDING",
+  "targetId": "16211908079305042870",
+  "targetLink": "https://www.googleapis.com/compute/v1/projects/project_name/zones/us-central1-a/instances/node-name/setMetadata",
+  "user": "foo@developer.gserviceaccount.com"
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7771e180/libcloud/test/compute/test_gce.py
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/test_gce.py b/libcloud/test/compute/test_gce.py
index 567276e..52c7125 100644
--- a/libcloud/test/compute/test_gce.py
+++ b/libcloud/test/compute/test_gce.py
@@ -19,8 +19,6 @@ import sys
 import unittest
 import datetime
 
-from mock import Mock
-
 from libcloud.utils.py3 import httplib
 from libcloud.compute.drivers.gce import (GCENodeDriver, API_VERSION,
                                           timestamp_to_datetime,
@@ -107,6 +105,12 @@ class GCENodeDriverTest(LibcloudTestCase, TestCaseMixin):
         image = self.driver._match_images(project, 'backports')
         self.assertEqual(image.name, 'backports-debian-7-wheezy-v20131127')
 
+    def test_ex_get_serial_output(self):
+        self.assertRaises(ValueError, self.driver.ex_get_serial_output, 'foo')
+        node = self.driver.ex_get_node('node-name', 'us-central1-a')
+        self.assertTrue(self.driver.ex_get_serial_output(node),
+                        'This is some serial\r\noutput for you.')
+
     def test_ex_list_addresses(self):
         address_list = self.driver.ex_list_addresses()
         address_list_all = self.driver.ex_list_addresses('all')
@@ -358,7 +362,7 @@ class GCENodeDriverTest(LibcloudTestCase, TestCaseMixin):
                                                                metadata,
                                                                boot_disk)
         self.assertEqual(node_request, '/zones/%s/instances' % location.name)
-        self.assertEqual(node_data['metadata'][0]['key'], 'test_key')
+        self.assertEqual(node_data['metadata']['items'][0]['key'], 'test_key')
         self.assertEqual(node_data['tags']['items'][0], 'libcloud')
         self.assertEqual(node_data['name'], 'lcnode')
         self.assertTrue(node_data['disks'][0]['boot'])
@@ -397,33 +401,102 @@ class GCENodeDriverTest(LibcloudTestCase, TestCaseMixin):
         self.assertTrue('https://www.googleapis.com/auth/compute.readonly'
                         in node_data['serviceAccounts'][0]['scopes'])
 
+    def test_format_metadata(self):
+        in_md = [{'key': 'k0', 'value': 'v0'}, {'key': 'k1', 'value': 'v1'}]
+        out_md = self.driver._format_metadata('fp', in_md)
+        self.assertTrue('fingerprint' in out_md)
+        self.assertEqual(out_md['fingerprint'], 'fp')
+        self.assertTrue('items' in out_md)
+        self.assertEqual(len(out_md['items']), 2)
+        self.assertTrue(out_md['items'][0]['key'] in ['k0', 'k1'])
+        self.assertTrue(out_md['items'][0]['value'] in ['v0', 'v1'])
+
+        in_md = [{'k0': 'v0'}, {'k1': 'v1'}]
+        out_md = self.driver._format_metadata('fp', in_md)
+        self.assertTrue('fingerprint' in out_md)
+        self.assertEqual(out_md['fingerprint'], 'fp')
+        self.assertTrue('items' in out_md)
+        self.assertEqual(len(out_md['items']), 2)
+        self.assertTrue(out_md['items'][0]['key'] in ['k0', 'k1'])
+        self.assertTrue(out_md['items'][0]['value'] in ['v0', 'v1'])
+
+        in_md = {'key': 'k0', 'value': 'v0'}
+        out_md = self.driver._format_metadata('fp', in_md)
+        self.assertTrue('fingerprint' in out_md)
+        self.assertEqual(out_md['fingerprint'], 'fp')
+        self.assertTrue('items' in out_md)
+        self.assertEqual(len(out_md['items']), 1, out_md)
+        self.assertEqual(out_md['items'][0]['key'], 'k0')
+        self.assertEqual(out_md['items'][0]['value'], 'v0')
+
+        in_md = {'k0': 'v0'}
+        out_md = self.driver._format_metadata('fp', in_md)
+        self.assertTrue('fingerprint' in out_md)
+        self.assertEqual(out_md['fingerprint'], 'fp')
+        self.assertTrue('items' in out_md)
+        self.assertEqual(len(out_md['items']), 1)
+        self.assertEqual(out_md['items'][0]['key'], 'k0')
+        self.assertEqual(out_md['items'][0]['value'], 'v0')
+
+        in_md = {'k0': 'v0', 'k1': 'v1', 'k2': 'v2'}
+        out_md = self.driver._format_metadata('fp', in_md)
+        self.assertTrue('fingerprint' in out_md)
+        self.assertEqual(out_md['fingerprint'], 'fp')
+        self.assertTrue('items' in out_md)
+        self.assertEqual(len(out_md['items']), 3)
+        keys = [x['key'] for x in out_md['items']]
+        vals = [x['value'] for x in out_md['items']]
+        keys.sort()
+        vals.sort()
+        self.assertTrue(keys, ['k0', 'k1', 'k2'])
+        self.assertTrue(vals, ['v0', 'v1', 'v2'])
+
+        in_md = {'items': [{'key': 'k0', 'value': 'v0'},
+                           {'key': 'k1', 'value': 'v1'}]}
+        out_md = self.driver._format_metadata('fp', in_md)
+        self.assertTrue('fingerprint' in out_md)
+        self.assertEqual(out_md['fingerprint'], 'fp')
+        self.assertTrue('items' in out_md)
+        self.assertEqual(len(out_md['items']), 2)
+        self.assertTrue(out_md['items'][0]['key'] in ['k0', 'k1'])
+        self.assertTrue(out_md['items'][0]['value'] in ['v0', 'v1'])
+
+        in_md = {'items': 'foo'}
+        self.assertRaises(ValueError, self.driver._format_metadata, 'fp', in_md)
+        in_md = {'items': {'key': 'k1', 'value': 'v0'}}
+        self.assertRaises(ValueError, self.driver._format_metadata, 'fp', in_md)
+        in_md = ['k0', 'v1']
+        self.assertRaises(ValueError, self.driver._format_metadata, 'fp', in_md)
+
     def test_create_node_with_metadata(self):
         node_name = 'node-name'
         image = self.driver.ex_get_image('debian-7')
         size = self.driver.ex_get_size('n1-standard-1')
-
-        self.driver._create_node_req = Mock()
-        self.driver._create_node_req.return_value = (None, None)
-        self.driver.connection.async_request = Mock()
-        self.driver.ex_get_node = Mock()
-
-        # ex_metadata doesn't contain "items" key
-        ex_metadata = {'key1': 'value1', 'key2': 'value2'}
-        self.driver.create_node(node_name, size, image,
-                                ex_metadata=ex_metadata)
-
-        actual = self.driver._create_node_req.call_args[0][6]
-        self.assertTrue('items' in actual)
-        self.assertEqual(len(actual['items']), 2)
-
-        # ex_metadata contains "items" key
-        ex_metadata = {'items': [{'key0': 'value0'}]}
-        self.driver.create_node(node_name, size, image,
-                                ex_metadata=ex_metadata)
-        actual = self.driver._create_node_req.call_args[0][6]
-        self.assertTrue('items' in actual)
-        self.assertEqual(len(actual['items']), 1)
-        self.assertEqual(actual['items'][0], {'key0': 'value0'})
+        zone = self.driver.ex_get_zone('us-central1-a')
+
+        # md is a list of dicts, each with 'key' and 'value' for
+        # backwards compatibility
+        md = [{'key': 'k0', 'value': 'v0'}, {'key': 'k1', 'value': 'v1'}]
+        request, data = self.driver._create_node_req(node_name, size, image,
+                                                     zone, metadata=md)
+        self.assertTrue('items' in data['metadata'])
+        self.assertEqual(len(data['metadata']['items']), 2)
+
+        # md doesn't contain "items" key
+        md = {'key': 'key1', 'value': 'value1'}
+        request, data = self.driver._create_node_req(node_name, size, image,
+                                                     zone, metadata=md)
+        self.assertTrue('items' in data['metadata'])
+        self.assertEqual(len(data['metadata']['items']), 1)
+
+        # md contains "items" key
+        md = {'items': [{'key': 'k0', 'value': 'v0'}]}
+        request, data = self.driver._create_node_req(node_name, size, image,
+                                                     zone, metadata=md)
+        self.assertTrue('items' in data['metadata'])
+        self.assertEqual(len(data['metadata']['items']), 1)
+        self.assertEqual(data['metadata']['items'][0]['key'], 'k0')
+        self.assertEqual(data['metadata']['items'][0]['value'], 'v0')
 
     def test_create_node_existing(self):
         node_name = 'libcloud-demo-europe-np-node'
@@ -587,6 +660,12 @@ class GCENodeDriverTest(LibcloudTestCase, TestCaseMixin):
         set_tags = self.driver.ex_set_node_tags(node, new_tags)
         self.assertTrue(set_tags)
 
+    def test_attach_volume_invalid_usecase(self):
+        node = self.driver.ex_get_node('node-name')
+        self.assertRaises(ValueError, self.driver.attach_volume, node, None)
+        self.assertRaises(ValueError, self.driver.attach_volume, node, None,
+                          ex_source='foo/bar', device=None)
+
     def test_attach_volume(self):
         volume = self.driver.ex_get_volume('lcdisk')
         node = self.driver.ex_get_node('node-name')
@@ -809,6 +888,18 @@ class GCENodeDriverTest(LibcloudTestCase, TestCaseMixin):
         self.assertTrue('bucketName' in project.extra['usageExportLocation'])
         self.assertTrue(project.extra['usageExportLocation']['bucketName'], 'gs://graphite-usage-reports')
 
+    def test_ex_add_access_config(self):
+        self.assertRaises(ValueError, self.driver.ex_add_access_config,
+                          'node', 'name')
+        node = self.driver.ex_get_node('node-name', 'us-central1-a')
+        self.assertTrue(self.driver.ex_add_access_config(node, 'foo'))
+
+    def test_ex_delete_access_config(self):
+        self.assertRaises(ValueError, self.driver.ex_add_access_config,
+                          'node', 'name', 'nic')
+        node = self.driver.ex_get_node('node-name', 'us-central1-a')
+        self.assertTrue(self.driver.ex_delete_access_config(node, 'foo', 'bar'))
+
     def test_ex_set_usage_export_bucket(self):
         self.assertRaises(ValueError,
                           self.driver.ex_set_usage_export_bucket, 'foo')
@@ -883,7 +974,7 @@ class GCENodeDriverTest(LibcloudTestCase, TestCaseMixin):
                           self.driver.ex_set_common_instance_metadata,
                           ['bad', 'type'])
         # test standard python dict
-        pydict = {'foo': 'pydict', 'one': 1}
+        pydict = {'key': 'pydict', 'value': 1}
         self.driver.ex_set_common_instance_metadata(pydict)
         # test GCE badly formatted dict
         bad_gcedict = {'items': 'foo'}
@@ -891,10 +982,27 @@ class GCENodeDriverTest(LibcloudTestCase, TestCaseMixin):
                           self.driver.ex_set_common_instance_metadata,
                           bad_gcedict)
         # test gce formatted dict
-        gcedict = {'items': [{'key': 'gcedict', 'value': 'v1'},
-                             {'key': 'gcedict', 'value': 'v2'}]}
+        gcedict = {'items': [{'key': 'gcedict1', 'value': 'v1'},
+                             {'key': 'gcedict2', 'value': 'v2'}]}
         self.driver.ex_set_common_instance_metadata(gcedict)
 
+    def test_ex_set_node_metadata(self):
+        node = self.driver.ex_get_node('node-name', 'us-central1-a')
+        # test non-dict
+        self.assertRaises(ValueError, self.driver.ex_set_node_metadata,
+                          node, ['bad', 'type'])
+        # test standard python dict
+        pydict = {'key': 'pydict', 'value': 1}
+        self.driver.ex_set_node_metadata(node, pydict)
+        # test GCE badly formatted dict
+        bad_gcedict = {'items': 'foo'}
+        self.assertRaises(ValueError, self.driver.ex_set_node_metadata,
+                          node, bad_gcedict)
+        # test gce formatted dict
+        gcedict = {'items': [{'key': 'gcedict1', 'value': 'v1'},
+                             {'key': 'gcedict2', 'value': 'v2'}]}
+        self.driver.ex_set_node_metadata(node, gcedict)
+
     def test_ex_get_region(self):
         region_name = 'us-central1'
         region = self.driver.ex_get_region(region_name)
@@ -985,6 +1093,10 @@ class GCEMockHttp(MockHttpTestCase):
             body = self.fixtures.load('setUsageExportBucket_post.json')
         return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
 
+    def _zones_us_central1_a_instances_node_name_setMetadata(self, method, url, body, headers):
+        body = self.fixtures.load('zones_us_central1_a_instances_node_name_setMetadata_post.json')
+        return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
+
     def _setCommonInstanceMetadata(self, method, url, body, headers):
         if method == 'POST':
             body = self.fixtures.load('setCommonInstanceMetadata_post.json')
@@ -1271,6 +1383,26 @@ class GCEMockHttp(MockHttpTestCase):
             'operations_operation_regions_us-central1_forwardingRules_lcforwardingrule_delete.json')
         return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
 
+    def _zones_us_central1_a_instances_node_name_deleteAccessConfig(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_post.json')
+        return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
+
+    def _zones_us_central1_a_instances_node_name_serialPort(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'zones_us-central1-a_instances_node_name_getSerialOutput.json')
+        return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
+
+    def _zones_us_central1_a_instances_node_name_addAccessConfig(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_post.json')
+        return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
+
+    def _zones_us_central1_a_operations_operation_setMetadata_post(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'operations_operation_zones_us_central1_a_node_name_setMetadata_post.json')
+        return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
+
     def _zones_us_central1_a_operations_operation_zones_us_central1_a_targetInstances_post(
             self, method, url, body, headers):
         body = self.fixtures.load(
@@ -1283,6 +1415,18 @@ class GCEMockHttp(MockHttpTestCase):
             'operations_operation_regions_us-central1_targetPools_post.json')
         return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
 
+    def _zones_us_central1_a_operations_operation_zones_us_central1_a_instances_node_name_addAccessConfig_post(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+            'operations_operation_zones_us-central1-a_instances_node_name_addAccessConfig_done.json')
+        return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
+
+    def _zones_us_central1_a_operations_operation_zones_us_central1_a_instances_node_name_deleteAccessConfig_post(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+            'operations_operation_zones_us-central1-a_instances_node_name_deleteAccessConfig_done.json')
+        return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
+
     def _zones_us_central1_a_operations_operation_zones_us_central1_a_targetInstances_lctargetinstance_delete(
             self, method, url, body, headers):
         body = self.fixtures.load(


Mime
View raw message