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 <[email protected]> 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 <[email protected]> Authored: Mon Dec 15 21:21:45 2014 +0000 Committer: Eric Johnson <[email protected]> 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": "[email protected]", + "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": "[email protected]", + "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": "[email protected]", + "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": "[email protected]", + "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": "[email protected]" +} 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": "[email protected]" +} 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(
