diff --git a/libcloud/common/linode.py b/libcloud/common/linode.py index 7f0d0e48f7..fb1fe24ec8 100644 --- a/libcloud/common/linode.py +++ b/libcloud/common/linode.py @@ -20,11 +20,7 @@ __all__ = [ "API_HOST", - "API_ROOT", "DEFAULT_API_VERSION", - "LinodeException", - "LinodeResponse", - "LinodeConnection", "LinodeResponseV4", "LinodeConnectionV4", "LinodeExceptionV4", @@ -34,151 +30,13 @@ # Endpoint for the Linode API API_HOST = "api.linode.com" -API_ROOT = "/" - DEFAULT_API_VERSION = "4.0" -# Constants that map a RAM figure to a PlanID (updated 2014-08-25) -LINODE_PLAN_IDS = { - 1024: "1", - 2048: "2", - 4096: "4", - 8192: "6", - 16384: "7", - 32768: "8", - 49152: "9", - 65536: "10", - 98304: "12", -} - # Available filesystems for disk creation -LINODE_DISK_FILESYSTEMS = ["ext3", "ext4", "swap", "raw"] LINODE_DISK_FILESYSTEMS_V4 = ["ext3", "ext4", "swap", "raw", "initrd"] -class LinodeException(Exception): - """Error originating from the Linode API - - This class wraps a Linode API error, a list of which is available in the - API documentation. All Linode API errors are a numeric code and a - human-readable description. - """ - - def __init__(self, code, message): - self.code = code - self.message = message - self.args = (code, message) - - def __str__(self): - return "(%u) %s" % (self.code, self.message) - - def __repr__(self): - return "" % (self.code, self.message) - - -class LinodeResponse(JsonResponse): - """ - Linode API response - - Wraps the HTTP response returned by the Linode API. - - libcloud does not take advantage of batching, so a response will always - reflect the above format. A few weird quirks are caught here as well. - """ - - objects = None - - def __init__(self, response, connection): - """Instantiate a LinodeResponse from the HTTP response - - :keyword response: The raw response returned by urllib - :return: parsed :class:`LinodeResponse`""" - self.errors = [] - super().__init__(response, connection) - - self.invalid = LinodeException(0xFF, "Invalid JSON received from server") - - # Move parse_body() to here; we can't be sure of failure until we've - # parsed the body into JSON. - self.objects, self.errors = self.parse_body() - - if not self.success(): - # Raise the first error, as there will usually only be one - raise self.errors[0] - - def parse_body(self): - """Parse the body of the response into JSON objects - - If the response chokes the parser, action and data will be returned as - None and errorarray will indicate an invalid JSON exception. - - :return: ``list`` of objects and ``list`` of errors""" - js = super().parse_body() - - try: - if isinstance(js, dict): - # solitary response - promote to list - js = [js] - ret = [] - errs = [] - for obj in js: - if "DATA" not in obj or "ERRORARRAY" not in obj or "ACTION" not in obj: - ret.append(None) - errs.append(self.invalid) - continue - ret.append(obj["DATA"]) - errs.extend(self._make_excp(e) for e in obj["ERRORARRAY"]) - return (ret, errs) - except Exception: - return (None, [self.invalid]) - - def success(self): - """Check the response for success - - The way we determine success is by the presence of an error in - ERRORARRAY. If one is there, we assume the whole request failed. - - :return: ``bool`` indicating a successful request""" - return len(self.errors) == 0 - - def _make_excp(self, error): - """Convert an API error to a LinodeException instance - - :keyword error: JSON object containing ``ERRORCODE`` and - ``ERRORMESSAGE`` - :type error: dict""" - if "ERRORCODE" not in error or "ERRORMESSAGE" not in error: - return None - if error["ERRORCODE"] == 4: - return InvalidCredsError(error["ERRORMESSAGE"]) - return LinodeException(error["ERRORCODE"], error["ERRORMESSAGE"]) - - -class LinodeConnection(ConnectionKey): - """ - A connection to the Linode API - - Wraps SSL connections to the Linode API, automagically injecting the - parameters that the API needs for each request. - """ - - host = API_HOST - responseCls = LinodeResponse - - def add_default_params(self, params): - """ - Add parameters that are necessary for every request - - This method adds ``api_key`` and ``api_responseFormat`` to - the request. - """ - params["api_key"] = self.key - # Be explicit about this in case the default changes. - params["api_responseFormat"] = "json" - return params - - class LinodeExceptionV4(Exception): def __init__(self, message): self.message = message @@ -187,7 +45,7 @@ def __str__(self): return "%s" % self.message def __repr__(self): - return "" % self.message + return "" % self.message class LinodeResponseV4(JsonResponse): diff --git a/libcloud/compute/drivers/linode.py b/libcloud/compute/drivers/linode.py index 81862cdb5a..b8da22ea49 100644 --- a/libcloud/compute/drivers/linode.py +++ b/libcloud/compute/drivers/linode.py @@ -16,22 +16,16 @@ """libcloud driver for the Linode(R) API This driver implements all libcloud functionality for the Linode API. -Since the API is a bit more fine-grained, create_node abstracts a significant -amount of work (and may take a while to run). Linode home page http://www.linode.com/ -Linode API documentation http://www.linode.com/api/ -Alternate bindings for reference http://github.com/tjfontaine/linode-python +Linode API documentation https://www.linode.com/docs/api/ Linode(R) is a registered trademark of Linode, LLC. """ -import os import re import binascii -import itertools -from copy import copy from datetime import datetime from libcloud.utils.py3 import httplib @@ -43,19 +37,12 @@ NodeDriver, NodeLocation, StorageVolume, - NodeAuthSSHKey, - NodeAuthPassword, ) from libcloud.common.linode import ( - API_ROOT, - LINODE_PLAN_IDS, DEFAULT_API_VERSION, - LINODE_DISK_FILESYSTEMS, LINODE_DISK_FILESYSTEMS_V4, LinodeDisk, - LinodeException, LinodeIPAddress, - LinodeConnection, LinodeExceptionV4, LinodeConnectionV4, ) @@ -85,9 +72,7 @@ def __new__( **kwargs, ): if cls is LinodeNodeDriver: - if api_version == "3.0": - cls = LinodeNodeDriverV3 - elif api_version == "4.0": + if api_version == "4.0": cls = LinodeNodeDriverV4 else: raise NotImplementedError( @@ -96,708 +81,6 @@ def __new__( return super().__new__(cls) -class LinodeNodeDriverV3(LinodeNodeDriver): - """libcloud driver for the Linode API - - Rough mapping of which is which: - - - list_nodes linode.list - - reboot_node linode.reboot - - destroy_node linode.delete - - create_node linode.create, linode.update, - linode.disk.createfromdistribution, - linode.disk.create, linode.config.create, - linode.ip.addprivate, linode.boot - - list_sizes avail.linodeplans - - list_images avail.distributions - - list_locations avail.datacenters - - list_volumes linode.disk.list - - destroy_volume linode.disk.delete - - For more information on the Linode API, be sure to read the reference: - - http://www.linode.com/api/ - """ - - connectionCls = LinodeConnection - _linode_plan_ids = LINODE_PLAN_IDS - _linode_disk_filesystems = LINODE_DISK_FILESYSTEMS - features = {"create_node": ["ssh_key", "password"]} - - def __init__( - self, - key, - secret=None, - secure=True, - host=None, - port=None, - api_version=None, - region=None, - **kwargs, - ): - """Instantiate the driver with the given API key - - :param key: the API key to use (required) - :type key: ``str`` - - :rtype: ``None`` - """ - self.datacenter = None - NodeDriver.__init__(self, key) - - # Converts Linode's state from DB to a NodeState constant. - LINODE_STATES = { - (-2): NodeState.UNKNOWN, # Boot Failed - (-1): NodeState.PENDING, # Being Created - 0: NodeState.PENDING, # Brand New - 1: NodeState.RUNNING, # Running - 2: NodeState.STOPPED, # Powered Off - 3: NodeState.REBOOTING, # Shutting Down - 4: NodeState.UNKNOWN, # Reserved - } - - def list_nodes(self): - """ - List all Linodes that the API key can access - - This call will return all Linodes that the API key in use has access - to. - If a node is in this list, rebooting will work; however, creation and - destruction are a separate grant. - - :return: List of node objects that the API key can access - :rtype: ``list`` of :class:`Node` - """ - params = {"api_action": "linode.list"} - data = self.connection.request(API_ROOT, params=params).objects[0] - return self._to_nodes(data) - - def start_node(self, node): - """ - Boot the given Linode - - """ - params = {"api_action": "linode.boot", "LinodeID": node.id} - self.connection.request(API_ROOT, params=params) - return True - - def stop_node(self, node): - """ - Shutdown the given Linode - - """ - params = {"api_action": "linode.shutdown", "LinodeID": node.id} - self.connection.request(API_ROOT, params=params) - return True - - def reboot_node(self, node): - """ - Reboot the given Linode - - Will issue a shutdown job followed by a boot job, using the last booted - configuration. In most cases, this will be the only configuration. - - :param node: the Linode to reboot - :type node: :class:`Node` - - :rtype: ``bool`` - """ - params = {"api_action": "linode.reboot", "LinodeID": node.id} - self.connection.request(API_ROOT, params=params) - return True - - def destroy_node(self, node): - """Destroy the given Linode - - Will remove the Linode from the account and issue a prorated credit. A - grant for removing Linodes from the account is required, otherwise this - method will fail. - - In most cases, all disk images must be removed from a Linode before the - Linode can be removed; however, this call explicitly skips those - safeguards. There is no going back from this method. - - :param node: the Linode to destroy - :type node: :class:`Node` - - :rtype: ``bool`` - """ - params = { - "api_action": "linode.delete", - "LinodeID": node.id, - "skipChecks": True, - } - self.connection.request(API_ROOT, params=params) - return True - - def create_node( - self, - name, - image, - size, - auth, - location=None, - ex_swap=None, - ex_rsize=None, - ex_kernel=None, - ex_payment=None, - ex_comment=None, - ex_private=False, - lconfig=None, - lroot=None, - lswap=None, - ): - """Create a new Linode, deploy a Linux distribution, and boot - - This call abstracts much of the functionality of provisioning a Linode - and getting it booted. A global grant to add Linodes to the account is - required, as this call will result in a billing charge. - - Note that there is a safety valve of 5 Linodes per hour, in order to - prevent a runaway script from ruining your day. - - :keyword name: the name to assign the Linode (mandatory) - :type name: ``str`` - - :keyword image: which distribution to deploy on the Linode (mandatory) - :type image: :class:`NodeImage` - - :keyword size: the plan size to create (mandatory) - :type size: :class:`NodeSize` - - :keyword auth: an SSH key or root password (mandatory) - :type auth: :class:`NodeAuthSSHKey` or :class:`NodeAuthPassword` - - :keyword location: which datacenter to create the Linode in - :type location: :class:`NodeLocation` - - :keyword ex_swap: size of the swap partition in MB (128) - :type ex_swap: ``int`` - - :keyword ex_rsize: size of the root partition in MB (plan size - swap). - :type ex_rsize: ``int`` - - :keyword ex_kernel: a kernel ID from avail.kernels (Latest 2.6 Stable). - :type ex_kernel: ``str`` - - :keyword ex_payment: one of 1, 12, or 24; subscription length (1) - :type ex_payment: ``int`` - - :keyword ex_comment: a small comment for the configuration (libcloud) - :type ex_comment: ``str`` - - :keyword ex_private: whether or not to request a private IP (False) - :type ex_private: ``bool`` - - :keyword lconfig: what to call the configuration (generated) - :type lconfig: ``str`` - - :keyword lroot: what to call the root image (generated) - :type lroot: ``str`` - - :keyword lswap: what to call the swap space (generated) - :type lswap: ``str`` - - :return: Node representing the newly-created Linode - :rtype: :class:`Node` - """ - auth = self._get_and_check_auth(auth) - - # Pick a location (resolves LIBCLOUD-41 in JIRA) - if location: - chosen = location.id - elif self.datacenter: - chosen = self.datacenter - else: - raise LinodeException(0xFB, "Need to select a datacenter first") - - # Step 0: Parameter validation before we purchase - # We're especially careful here so we don't fail after purchase, rather - # than getting halfway through the process and having the API fail. - - # Plan ID - plans = self.list_sizes() - if size.id not in [p.id for p in plans]: - raise LinodeException(0xFB, "Invalid plan ID -- avail.plans") - - # Payment schedule - payment = "1" if not ex_payment else str(ex_payment) - if payment not in ["1", "12", "24"]: - raise LinodeException(0xFB, "Invalid subscription (1, 12, 24)") - - ssh = None - root = None - # SSH key and/or root password - if isinstance(auth, NodeAuthSSHKey): - ssh = auth.pubkey # pylint: disable=no-member - elif isinstance(auth, NodeAuthPassword): - root = auth.password - - if not ssh and not root: - raise LinodeException(0xFB, "Need SSH key or root password") - if root is not None and len(root) < 6: - raise LinodeException(0xFB, "Root password is too short") - - # Swap size - try: - swap = 128 if not ex_swap else int(ex_swap) - except Exception: - raise LinodeException(0xFB, "Need an integer swap size") - - # Root partition size - imagesize = (size.disk - swap) if not ex_rsize else int(ex_rsize) - if (imagesize + swap) > size.disk: - raise LinodeException(0xFB, "Total disk images are too big") - - # Distribution ID - distros = self.list_images() - if image.id not in [d.id for d in distros]: - raise LinodeException(0xFB, "Invalid distro -- avail.distributions") - - # Kernel - if ex_kernel: - kernel = ex_kernel - else: - if image.extra["64bit"]: - # For a list of available kernel ids, see - # https://www.linode.com/kernels/ - kernel = 138 - else: - kernel = 137 - params = {"api_action": "avail.kernels"} - kernels = self.connection.request(API_ROOT, params=params).objects[0] - if kernel not in [z["KERNELID"] for z in kernels]: - raise LinodeException(0xFB, "Invalid kernel -- avail.kernels") - - # Comments - comments = ( - "Created by Apache libcloud " - if not ex_comment - else ex_comment - ) - - # Step 1: linode.create - params = { - "api_action": "linode.create", - "DatacenterID": chosen, - "PlanID": size.id, - "PaymentTerm": payment, - } - data = self.connection.request(API_ROOT, params=params).objects[0] - linode = {"id": data["LinodeID"]} - - # Step 1b. linode.update to rename the Linode - params = { - "api_action": "linode.update", - "LinodeID": linode["id"], - "Label": name, - } - self.connection.request(API_ROOT, params=params) - - # Step 1c. linode.ip.addprivate if it was requested - if ex_private: - params = {"api_action": "linode.ip.addprivate", "LinodeID": linode["id"]} - self.connection.request(API_ROOT, params=params) - - # Step 1d. Labels - # use the linode id as the name can be up to 63 chars and the labels - # are limited to 48 chars - label = { - "lconfig": "[%s] Configuration Profile" % linode["id"], - "lroot": "[{}] {} Disk Image".format(linode["id"], image.name), - "lswap": "[%s] Swap Space" % linode["id"], - } - - if lconfig: - label["lconfig"] = lconfig - - if lroot: - label["lroot"] = lroot - - if lswap: - label["lswap"] = lswap - - # Step 2: linode.disk.createfromdistribution - if not root: - root = binascii.b2a_base64(os.urandom(8)).decode("ascii").strip() - - params = { - "api_action": "linode.disk.createfromdistribution", - "LinodeID": linode["id"], - "DistributionID": image.id, - "Label": label["lroot"], - "Size": imagesize, - "rootPass": root, - } - if ssh: - params["rootSSHKey"] = ssh - data = self.connection.request(API_ROOT, params=params).objects[0] - linode["rootimage"] = data["DiskID"] - - # Step 3: linode.disk.create for swap - params = { - "api_action": "linode.disk.create", - "LinodeID": linode["id"], - "Label": label["lswap"], - "Type": "swap", - "Size": swap, - } - data = self.connection.request(API_ROOT, params=params).objects[0] - linode["swapimage"] = data["DiskID"] - - # Step 4: linode.config.create for main profile - disks = "{},{},,,,,,,".format(linode["rootimage"], linode["swapimage"]) - params = { - "api_action": "linode.config.create", - "LinodeID": linode["id"], - "KernelID": kernel, - "Label": label["lconfig"], - "Comments": comments, - "DiskList": disks, - } - if ex_private: - params["helper_network"] = True - params["helper_distro"] = True - - data = self.connection.request(API_ROOT, params=params).objects[0] - linode["config"] = data["ConfigID"] - - # Step 5: linode.boot - params = { - "api_action": "linode.boot", - "LinodeID": linode["id"], - "ConfigID": linode["config"], - } - self.connection.request(API_ROOT, params=params) - - # Make a node out of it and hand it back - params = {"api_action": "linode.list", "LinodeID": linode["id"]} - data = self.connection.request(API_ROOT, params=params).objects[0] - nodes = self._to_nodes(data) - - if len(nodes) == 1: - node = nodes[0] - if getattr(auth, "generated", False): - node.extra["password"] = auth.password - return node - - return None - - def ex_resize_node(self, node, size): - """Resizes a Linode from one plan to another - - Immediately shuts the Linode down, charges/credits the account, - and issue a migration to another host server. - Requires a size (numeric), which is the desired PlanID available from - avail.LinodePlans() - After resize is complete the node needs to be booted - """ - - params = {"api_action": "linode.resize", "LinodeID": node.id, "PlanID": size} - self.connection.request(API_ROOT, params=params) - return True - - def ex_start_node(self, node): - # NOTE: This method is here for backward compatibility reasons after - # this method was promoted to be part of the standard compute API in - # Libcloud v2.7.0 - return self.start_node(node=node) - - def ex_stop_node(self, node): - # NOTE: This method is here for backward compatibility reasons after - # this method was promoted to be part of the standard compute API in - # Libcloud v2.7.0 - return self.stop_node(node=node) - - def ex_rename_node(self, node, name): - """Renames a node""" - - params = {"api_action": "linode.update", "LinodeID": node.id, "Label": name} - self.connection.request(API_ROOT, params=params) - return True - - def list_sizes(self, location=None): - """ - List available Linode plans - - Gets the sizes that can be used for creating a Linode. Since available - Linode plans vary per-location, this method can also be passed a - location to filter the availability. - - :keyword location: the facility to retrieve plans in - :type location: :class:`NodeLocation` - - :rtype: ``list`` of :class:`NodeSize` - """ - params = {"api_action": "avail.linodeplans"} - data = self.connection.request(API_ROOT, params=params).objects[0] - sizes = [] - for obj in data: - n = NodeSize( - id=obj["PLANID"], - name=obj["LABEL"], - ram=obj["RAM"], - disk=(obj["DISK"] * 1024), - bandwidth=obj["XFER"], - price=obj["PRICE"], - driver=self.connection.driver, - ) - sizes.append(n) - return sizes - - def list_images(self): - """ - List available Linux distributions - - Retrieve all Linux distributions that can be deployed to a Linode. - - :rtype: ``list`` of :class:`NodeImage` - """ - params = {"api_action": "avail.distributions"} - data = self.connection.request(API_ROOT, params=params).objects[0] - distros = [] - for obj in data: - i = NodeImage( - id=obj["DISTRIBUTIONID"], - name=obj["LABEL"], - driver=self.connection.driver, - extra={"pvops": obj["REQUIRESPVOPSKERNEL"], "64bit": obj["IS64BIT"]}, - ) - distros.append(i) - return distros - - def list_locations(self): - """ - List available facilities for deployment - - Retrieve all facilities that a Linode can be deployed in. - - :rtype: ``list`` of :class:`NodeLocation` - """ - params = {"api_action": "avail.datacenters"} - data = self.connection.request(API_ROOT, params=params).objects[0] - nl = [] - for dc in data: - country = None - if "USA" in dc["LOCATION"]: - country = "US" - elif "UK" in dc["LOCATION"]: - country = "GB" - elif "JP" in dc["LOCATION"]: - country = "JP" - else: - country = "??" - nl.append(NodeLocation(dc["DATACENTERID"], dc["LOCATION"], country, self)) - return nl - - def linode_set_datacenter(self, dc): - """ - Set the default datacenter for Linode creation - - Since Linodes must be created in a facility, this function sets the - default that :class:`create_node` will use. If a location keyword is - not passed to :class:`create_node`, this method must have already been - used. - - :keyword dc: the datacenter to create Linodes in unless specified - :type dc: :class:`NodeLocation` - - :rtype: ``bool`` - """ - did = dc.id - params = {"api_action": "avail.datacenters"} - data = self.connection.request(API_ROOT, params=params).objects[0] - for datacenter in data: - if did == dc["DATACENTERID"]: - self.datacenter = did - return - - dcs = ", ".join([d["DATACENTERID"] for d in data]) - self.datacenter = None - raise LinodeException(0xFD, "Invalid datacenter (use one of %s)" % dcs) - - def destroy_volume(self, volume): - """ - Destroys disk volume for the Linode. Linode id is to be provided as - extra["LinodeId"] within :class:`StorageVolume`. It can be retrieved - by :meth:`libcloud.compute.drivers.linode.LinodeNodeDriver\ - .ex_list_volumes`. - - :param volume: Volume to be destroyed - :type volume: :class:`StorageVolume` - - :rtype: ``bool`` - """ - if not isinstance(volume, StorageVolume): - raise LinodeException(0xFD, "Invalid volume instance") - - if volume.extra["LINODEID"] is None: - raise LinodeException(0xFD, "Missing LinodeID") - - params = { - "api_action": "linode.disk.delete", - "LinodeID": volume.extra["LINODEID"], - "DiskID": volume.id, - } - self.connection.request(API_ROOT, params=params) - - return True - - def ex_create_volume(self, size, name, node, fs_type): - """ - Create disk for the Linode. - - :keyword size: Size of volume in megabytes (required) - :type size: ``int`` - - :keyword name: Name of the volume to be created - :type name: ``str`` - - :keyword node: Node to attach volume to. - :type node: :class:`Node` - - :keyword fs_type: The formatted type of this disk. Valid types are: - ext3, ext4, swap, raw - :type fs_type: ``str`` - - - :return: StorageVolume representing the newly-created volume - :rtype: :class:`StorageVolume` - """ - # check node - if not isinstance(node, Node): - raise LinodeException(0xFD, "Invalid node instance") - - # check space available - total_space = node.extra["TOTALHD"] - existing_volumes = self.ex_list_volumes(node) - used_space = 0 - for volume in existing_volumes: - used_space = used_space + volume.size - - available_space = total_space - used_space - if available_space < size: - raise LinodeException( - 0xFD, - "Volume size too big. Available space\ - %d" % available_space, - ) - - # check filesystem type - if fs_type not in self._linode_disk_filesystems: - raise LinodeException(0xFD, "Not valid filesystem type") - - params = { - "api_action": "linode.disk.create", - "LinodeID": node.id, - "Label": name, - "Type": fs_type, - "Size": size, - } - data = self.connection.request(API_ROOT, params=params).objects[0] - volume = data["DiskID"] - # Make a volume out of it and hand it back - params = { - "api_action": "linode.disk.list", - "LinodeID": node.id, - "DiskID": volume, - } - data = self.connection.request(API_ROOT, params=params).objects[0] - return self._to_volumes(data)[0] - - def ex_list_volumes(self, node, disk_id=None): - """ - List existing disk volumes for for given Linode. - - :keyword node: Node to list disk volumes for. (required) - :type node: :class:`Node` - - :keyword disk_id: Id for specific disk volume. (optional) - :type disk_id: ``int`` - - :rtype: ``list`` of :class:`StorageVolume` - """ - if not isinstance(node, Node): - raise LinodeException(0xFD, "Invalid node instance") - - params = {"api_action": "linode.disk.list", "LinodeID": node.id} - # Add param if disk_id was specified - if disk_id is not None: - params["DiskID"] = disk_id - - data = self.connection.request(API_ROOT, params=params).objects[0] - return self._to_volumes(data) - - def _to_volumes(self, objs): - """ - Convert returned JSON volumes into StorageVolume instances - - :keyword objs: ``list`` of JSON dictionaries representing the - StorageVolumes - :type objs: ``list`` - - :return: ``list`` of :class:`StorageVolume`s - """ - volumes = {} - for o in objs: - vid = o["DISKID"] - volumes[vid] = vol = StorageVolume( - id=vid, - name=o["LABEL"], - size=int(o["SIZE"]), - driver=self.connection.driver, - ) - vol.extra = copy(o) - return list(volumes.values()) - - def _to_nodes(self, objs): - """Convert returned JSON Linodes into Node instances - - :keyword objs: ``list`` of JSON dictionaries representing the Linodes - :type objs: ``list`` - :return: ``list`` of :class:`Node`s""" - - # Get the IP addresses for the Linodes - nodes = {} - batch = [] - for o in objs: - lid = o["LINODEID"] - nodes[lid] = n = Node( - id=lid, - name=o["LABEL"], - public_ips=[], - private_ips=[], - state=self.LINODE_STATES[o["STATUS"]], - driver=self.connection.driver, - ) - n.extra = copy(o) - n.extra["PLANID"] = self._linode_plan_ids.get(o.get("TOTALRAM")) - batch.append({"api_action": "linode.ip.list", "LinodeID": lid}) - - # Avoid batch limitation - ip_answers = [] - args = [iter(batch)] * 25 - - for twenty_five in itertools.zip_longest(*args): - twenty_five = [q for q in twenty_five if q] - params = { - "api_action": "batch", - "api_requestArray": json.dumps(twenty_five), - } - req = self.connection.request(API_ROOT, params=params) - if not req.success() or len(req.objects) == 0: - return None - ip_answers.extend(req.objects) - - # Add the returned IPs to the nodes and return them - for ip_list in ip_answers: - for ip in ip_list: - lid = ip["LINODEID"] - which = nodes[lid].public_ips if ip["ISPUBLIC"] == 1 else nodes[lid].private_ips - which.append(ip["IPADDRESS"]) - return list(nodes.values()) - - class LinodeNodeDriverV4(LinodeNodeDriver): connectionCls = LinodeConnectionV4 _linode_disk_filesystems = LINODE_DISK_FILESYSTEMS_V4 diff --git a/libcloud/dns/drivers/linode.py b/libcloud/dns/drivers/linode.py index b2f609f254..46429fac90 100644 --- a/libcloud/dns/drivers/linode.py +++ b/libcloud/dns/drivers/linode.py @@ -18,15 +18,11 @@ from datetime import datetime from libcloud.dns.base import Zone, Record, DNSDriver -from libcloud.dns.types import Provider, RecordType, ZoneDoesNotExistError, RecordDoesNotExistError +from libcloud.dns.types import Provider, RecordType from libcloud.utils.py3 import httplib -from libcloud.utils.misc import get_new_obj, merge_valid_keys +from libcloud.utils.misc import merge_valid_keys from libcloud.common.linode import ( - API_ROOT, DEFAULT_API_VERSION, - LinodeResponse, - LinodeException, - LinodeConnection, LinodeResponseV4, LinodeExceptionV4, LinodeConnectionV4, @@ -38,18 +34,8 @@ import json -VALID_ZONE_EXTRA_PARAMS = [ - "SOA_Email", - "Refresh_sec", - "Retry_sec", - "Expire_sec", - "status", - "master_ips", -] - -VALID_RECORD_EXTRA_PARAMS = ["Priority", "Weight", "Port", "Protocol", "TTL_sec"] - VALID_ZONE_EXTRA_PARAMS_V4 = [ + "axfr_ips", "description", "expire_sec", "master_ips", @@ -88,9 +74,7 @@ def __new__( **kwargs, ): if cls is LinodeDNSDriver: - if api_version == "3.0": - cls = LinodeDNSDriverV3 - elif api_version == "4.0": + if api_version == "4.0": cls = LinodeDNSDriverV4 else: raise NotImplementedError( @@ -99,276 +83,6 @@ def __new__( return super().__new__(cls) -class LinodeDNSResponse(LinodeResponse): - def _make_excp(self, error): - result = super()._make_excp(error) - if isinstance(result, LinodeException) and result.code == 5: - context = self.connection.context - - if context["resource"] == "zone": - result = ZoneDoesNotExistError( - value="", driver=self.connection.driver, zone_id=context["id"] - ) - - elif context["resource"] == "record": - result = RecordDoesNotExistError( - value="", driver=self.connection.driver, record_id=context["id"] - ) - return result - - -class LinodeDNSConnection(LinodeConnection): - responseCls = LinodeDNSResponse - - -class LinodeDNSDriverV3(LinodeDNSDriver): - connectionCls = LinodeDNSConnection - - RECORD_TYPE_MAP = { - RecordType.NS: "NS", - RecordType.MX: "MX", - RecordType.A: "A", - RecordType.AAAA: "AAAA", - RecordType.CNAME: "CNAME", - RecordType.TXT: "TXT", - RecordType.SRV: "SRV", - } - - def list_zones(self): - params = {"api_action": "domain.list"} - data = self.connection.request(API_ROOT, params=params).objects[0] - zones = self._to_zones(data) - return zones - - def list_records(self, zone): - params = {"api_action": "domain.resource.list", "DOMAINID": zone.id} - - self.connection.set_context(context={"resource": "zone", "id": zone.id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - records = self._to_records(items=data, zone=zone) - return records - - def get_zone(self, zone_id): - params = {"api_action": "domain.list", "DomainID": zone_id} - self.connection.set_context(context={"resource": "zone", "id": zone_id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - zones = self._to_zones(data) - - if len(zones) != 1: - raise ZoneDoesNotExistError(value="", driver=self, zone_id=zone_id) - - return zones[0] - - def get_record(self, zone_id, record_id): - zone = self.get_zone(zone_id=zone_id) - params = { - "api_action": "domain.resource.list", - "DomainID": zone_id, - "ResourceID": record_id, - } - self.connection.set_context(context={"resource": "record", "id": record_id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - records = self._to_records(items=data, zone=zone) - - if len(records) != 1: - raise RecordDoesNotExistError(value="", driver=self, record_id=record_id) - - return records[0] - - def create_zone(self, domain, type="master", ttl=None, extra=None): - """ - Create a new zone. - - API docs: http://www.linode.com/api/dns/domain.create - """ - params = {"api_action": "domain.create", "Type": type, "Domain": domain} - - if ttl: - params["TTL_sec"] = ttl - - merged = merge_valid_keys(params=params, valid_keys=VALID_ZONE_EXTRA_PARAMS, extra=extra) - data = self.connection.request(API_ROOT, params=params).objects[0] - zone = Zone( - id=data["DomainID"], - domain=domain, - type=type, - ttl=ttl, - extra=merged, - driver=self, - ) - return zone - - def update_zone(self, zone, domain=None, type=None, ttl=None, extra=None): - """ - Update an existing zone. - - API docs: http://www.linode.com/api/dns/domain.update - """ - params = {"api_action": "domain.update", "DomainID": zone.id} - - if type: - params["Type"] = type - - if domain: - params["Domain"] = domain - - if ttl: - params["TTL_sec"] = ttl - - merged = merge_valid_keys(params=params, valid_keys=VALID_ZONE_EXTRA_PARAMS, extra=extra) - self.connection.request(API_ROOT, params=params).objects[0] - updated_zone = get_new_obj( - obj=zone, - klass=Zone, - attributes={"domain": domain, "type": type, "ttl": ttl, "extra": merged}, - ) - return updated_zone - - def create_record(self, name, zone, type, data, extra=None): - """ - Create a new record. - - API docs: http://www.linode.com/api/dns/domain.resource.create - """ - params = { - "api_action": "domain.resource.create", - "DomainID": zone.id, - "Name": name, - "Target": data, - "Type": self.RECORD_TYPE_MAP[type], - } - merged = merge_valid_keys(params=params, valid_keys=VALID_RECORD_EXTRA_PARAMS, extra=extra) - - result = self.connection.request(API_ROOT, params=params).objects[0] - record = Record( - id=result["ResourceID"], - name=name, - type=type, - data=data, - extra=merged, - zone=zone, - driver=self, - ttl=merged.get("TTL_sec", None), - ) - return record - - def update_record(self, record, name=None, type=None, data=None, extra=None): - """ - Update an existing record. - - API docs: http://www.linode.com/api/dns/domain.resource.update - """ - params = { - "api_action": "domain.resource.update", - "ResourceID": record.id, - "DomainID": record.zone.id, - } - - if name: - params["Name"] = name - - if data: - params["Target"] = data - - if type is not None: - params["Type"] = self.RECORD_TYPE_MAP[type] - - merged = merge_valid_keys(params=params, valid_keys=VALID_RECORD_EXTRA_PARAMS, extra=extra) - - self.connection.request(API_ROOT, params=params).objects[0] - updated_record = get_new_obj( - obj=record, - klass=Record, - attributes={"name": name, "data": data, "type": type, "extra": merged}, - ) - return updated_record - - def delete_zone(self, zone): - params = {"api_action": "domain.delete", "DomainID": zone.id} - - self.connection.set_context(context={"resource": "zone", "id": zone.id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - - return "DomainID" in data - - def delete_record(self, record): - params = { - "api_action": "domain.resource.delete", - "DomainID": record.zone.id, - "ResourceID": record.id, - } - - self.connection.set_context(context={"resource": "record", "id": record.id}) - data = self.connection.request(API_ROOT, params=params).objects[0] - - return "ResourceID" in data - - def _to_zones(self, items): - """ - Convert a list of items to the Zone objects. - """ - zones = [] - - for item in items: - zones.append(self._to_zone(item)) - - return zones - - def _to_zone(self, item): - """ - Build an Zone object from the item dictionary. - """ - extra = { - "SOA_Email": item["SOA_EMAIL"], - "status": item["STATUS"], - "description": item["DESCRIPTION"], - } - zone = Zone( - id=item["DOMAINID"], - domain=item["DOMAIN"], - type=item["TYPE"], - ttl=item["TTL_SEC"], - driver=self, - extra=extra, - ) - return zone - - def _to_records(self, items, zone=None): - """ - Convert a list of items to the Record objects. - """ - records = [] - - for item in items: - records.append(self._to_record(item=item, zone=zone)) - - return records - - def _to_record(self, item, zone=None): - """ - Build a Record object from the item dictionary. - """ - extra = { - "protocol": item["PROTOCOL"], - "ttl_sec": item["TTL_SEC"], - "port": item["PORT"], - "weight": item["WEIGHT"], - "priority": item["PRIORITY"], - } - type = self._string_to_record_type(item["TYPE"]) - record = Record( - id=item["RESOURCEID"], - name=item["NAME"], - type=type, - data=item["TARGET"], - zone=zone, - driver=self, - ttl=item["TTL_SEC"], - extra=extra, - ) - return record - - class LinodeDNSResponseV4(LinodeResponseV4): pass @@ -387,6 +101,7 @@ class LinodeDNSDriverV4(LinodeDNSDriver): RecordType.A: "A", RecordType.AAAA: "AAAA", RecordType.CNAME: "CNAME", + RecordType.PTR: "PTR", RecordType.TXT: "TXT", RecordType.SRV: "SRV", RecordType.CAA: "CAA", @@ -461,7 +176,7 @@ def create_zone(self, domain, type="master", ttl=None, extra=None): :keyword ttl: TTL for new records. (optional) :type ttl: ``int`` - :keyword extra: Extra attributes.('description', 'expire_sec', \ + :keyword extra: Extra attributes.('axfr_ips', 'description', 'expire_sec', \ 'master_ips','refresh_sec', 'retry_sec', 'soa_email',\ 'status', 'tags'). 'soa_email' required for master zones :type extra: ``dict`` @@ -537,7 +252,7 @@ def update_zone(self, zone, domain, type="master", ttl=None, extra=None): :param ttl: TTL for new records. (optional) :type ttl: ``int`` - :param extra: Extra attributes ('description', 'expire_sec', \ + :param extra: Extra attributes ('axfr_ips', 'description', 'expire_sec', \ 'master_ips','refresh_sec', 'retry_sec', 'soa_email','status', 'tags') :type extra: ``dict`` @@ -654,6 +369,7 @@ def _to_record(self, item, zone=None): "priority": item["priority"], "service": item["service"], "protocol": item["protocol"], + "tag": item["tag"], "created": self._to_datetime(item["created"]), "updated": self._to_datetime(item["updated"]), } diff --git a/libcloud/test/compute/fixtures/linode/_avail_datacenters.json b/libcloud/test/compute/fixtures/linode/_avail_datacenters.json deleted file mode 100644 index eb169623b1..0000000000 --- a/libcloud/test/compute/fixtures/linode/_avail_datacenters.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "LOCATION": "Dallas, TX, USA", - "DATACENTERID": 2, - "ABBR": "dallas" - }, - { - "LOCATION": "Fremont, CA, USA", - "DATACENTERID": 3, - "ABBR": "fremont" - }, - { - "LOCATION": "Atlanta, GA, USA", - "DATACENTERID": 4, - "ABBR": "atlanta" - }, - { - "LOCATION": "Newark, NJ, USA", - "DATACENTERID": 6, - "ABBR": "newark" - }, - { - "LOCATION": "London, England, UK", - "DATACENTERID": 7, - "ABBR": "london" - }, - { - "LOCATION": "Tokyo, JP", - "DATACENTERID": 8, - "ABBR": "tokyo" - } - ], - "ACTION": "avail.datacenters" -} \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/linode/_avail_distributions.json b/libcloud/test/compute/fixtures/linode/_avail_distributions.json deleted file mode 100644 index f1bdee61b9..0000000000 --- a/libcloud/test/compute/fixtures/linode/_avail_distributions.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 112, - "IS64BIT": 1, - "LABEL": "Arch Linux 2013.06", - "MINIMAGESIZE": 500, - "CREATE_DT": "2013-06-06 02:45:11.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 89, - "IS64BIT": 1, - "LABEL": "CentOS 6.2", - "MINIMAGESIZE": 800, - "CREATE_DT": "2011-07-19 11:38:20.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 78, - "IS64BIT": 1, - "LABEL": "Debian 6", - "MINIMAGESIZE": 550, - "CREATE_DT": "2011-02-08 16:54:31.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 109, - "IS64BIT": 1, - "LABEL": "Debian 7", - "MINIMAGESIZE": 660, - "CREATE_DT": "2013-05-08 11:31:32.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 114, - "IS64BIT": 1, - "LABEL": "Fedora 19", - "MINIMAGESIZE": 750, - "CREATE_DT": "2013-08-26 15:29:21.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 53, - "IS64BIT": 1, - "LABEL": "Gentoo", - "MINIMAGESIZE": 1000, - "CREATE_DT": "2009-04-04 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 115, - "IS64BIT": 1, - "LABEL": "openSUSE 12.3", - "MINIMAGESIZE": 1024, - "CREATE_DT": "2013-09-19 10:49:09.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 87, - "IS64BIT": 1, - "LABEL": "Slackware 13.37", - "MINIMAGESIZE": 600, - "CREATE_DT": "2011-06-05 15:11:59.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 65, - "IS64BIT": 1, - "LABEL": "Ubuntu 10.04 LTS", - "MINIMAGESIZE": 450, - "CREATE_DT": "2010-04-29 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 99, - "IS64BIT": 1, - "LABEL": "Ubuntu 12.04 LTS", - "MINIMAGESIZE": 600, - "CREATE_DT": "2012-04-26 17:25:16.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 111, - "IS64BIT": 1, - "LABEL": "Ubuntu 13.04", - "MINIMAGESIZE": 770, - "CREATE_DT": "2013-05-08 11:31:32.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 113, - "IS64BIT": 0, - "LABEL": "Arch Linux 2013.06 32bit", - "MINIMAGESIZE": 500, - "CREATE_DT": "2013-06-06 02:45:11.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 88, - "IS64BIT": 0, - "LABEL": "CentOS 6.2 32bit", - "MINIMAGESIZE": 800, - "CREATE_DT": "2011-07-19 11:38:20.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 77, - "IS64BIT": 0, - "LABEL": "Debian 6 32bit", - "MINIMAGESIZE": 550, - "CREATE_DT": "2011-02-08 16:54:31.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 108, - "IS64BIT": 0, - "LABEL": "Debian 7 32bit", - "MINIMAGESIZE": 660, - "CREATE_DT": "2013-05-08 11:31:32.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 72, - "IS64BIT": 0, - "LABEL": "Gentoo 32bit", - "MINIMAGESIZE": 1000, - "CREATE_DT": "2010-09-13 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 86, - "IS64BIT": 0, - "LABEL": "Slackware 13.37 32bit", - "MINIMAGESIZE": 600, - "CREATE_DT": "2011-06-05 15:11:59.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 64, - "IS64BIT": 0, - "LABEL": "Ubuntu 10.04 LTS 32bit", - "MINIMAGESIZE": 450, - "CREATE_DT": "2010-04-29 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 98, - "IS64BIT": 0, - "LABEL": "Ubuntu 12.04 LTS 32bit", - "MINIMAGESIZE": 600, - "CREATE_DT": "2012-04-26 17:25:16.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 110, - "IS64BIT": 0, - "LABEL": "Ubuntu 13.04 32bit", - "MINIMAGESIZE": 770, - "CREATE_DT": "2013-05-08 11:31:32.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 105, - "IS64BIT": 1, - "LABEL": "Arch Linux 2012.10", - "MINIMAGESIZE": 500, - "CREATE_DT": "2012-10-22 15:00:49.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 60, - "IS64BIT": 1, - "LABEL": "CentOS 5.6 64bit", - "MINIMAGESIZE": 950, - "CREATE_DT": "2009-08-17 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 100, - "IS64BIT": 1, - "LABEL": "Fedora 17", - "MINIMAGESIZE": 800, - "CREATE_DT": "2012-05-31 16:03:49.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 97, - "IS64BIT": 1, - "LABEL": "openSUSE 12.1", - "MINIMAGESIZE": 1000, - "CREATE_DT": "2012-04-13 11:43:30.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 107, - "IS64BIT": 1, - "LABEL": "Ubuntu 12.10", - "MINIMAGESIZE": 660, - "CREATE_DT": "2012-11-06 11:51:25.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 104, - "IS64BIT": 0, - "LABEL": "Arch Linux 2012.10 32bit", - "MINIMAGESIZE": 500, - "CREATE_DT": "2012-10-22 15:00:49.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 59, - "IS64BIT": 0, - "LABEL": "CentOS 5.6 32bit", - "MINIMAGESIZE": 950, - "CREATE_DT": "2009-08-17 00:00:00.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 101, - "IS64BIT": 0, - "LABEL": "Fedora 17 32bit", - "MINIMAGESIZE": 800, - "CREATE_DT": "2012-05-31 16:03:49.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 96, - "IS64BIT": 0, - "LABEL": "openSUSE 12.1 32bit", - "MINIMAGESIZE": 1000, - "CREATE_DT": "2012-04-13 11:43:30.0" - }, - { - "REQUIRESPVOPSKERNEL": 1, - "DISTRIBUTIONID": 106, - "IS64BIT": 0, - "LABEL": "Ubuntu 12.10 32bit", - "MINIMAGESIZE": 660, - "CREATE_DT": "2012-11-06 11:51:25.0" - } - ], - "ACTION": "avail.distributions" -} \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/linode/_avail_kernels.json b/libcloud/test/compute/fixtures/linode/_avail_kernels.json deleted file mode 100644 index 9552c46c5f..0000000000 --- a/libcloud/test/compute/fixtures/linode/_avail_kernels.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "ERRORARRAY": [], - "ACTION": "avail.kernels", - "DATA": [ - { - "LABEL": "Latest 2.6 Stable (2.6.18.8-linode19)", - "ISXEN": 1, - "KERNELID": 60 - }, - { - "LABEL": "2.6.18.8-linode19", - "ISXEN": 1, - "KERNELID": 103 - }, - { - "LABEL": "2.6.30.5-linode20", - "ISXEN": 1, - "KERNELID": 105 - }, - { - "LABEL": "Latest 2.6 Stable (2.6.18.8-x86_64-linode7)", - "ISXEN": 1, - "KERNELID": 107 - }, - { - "LABEL": "2.6.18.8-x86_64-linode7", - "ISXEN": 1, - "KERNELID": 104 - }, - { - "LABEL": "2.6.30.5-x86_64-linode8", - "ISXEN": 1, - "KERNELID": 106 - }, - { - "LABEL": "pv-grub-x86_32", - "ISXEN": 1, - "KERNELID": 92 - }, - { - "LABEL": "pv-grub-x86_64", - "ISXEN": 1, - "KERNELID": 95 - }, - { - "LABEL": "Recovery - Finnix (kernel)", - "ISXEN": 1, - "KERNELID": 61 - }, - { - "LABEL": "2.6.18.8-domU-linode7", - "ISXEN": 1, - "KERNELID": 81 - }, - { - "LABEL": "2.6.18.8-linode10", - "ISXEN": 1, - "KERNELID": 89 - }, - { - "LABEL": "2.6.18.8-linode16", - "ISXEN": 1, - "KERNELID": 98 - }, - { - "LABEL": "2.6.24.4-linode8", - "ISXEN": 1, - "KERNELID": 84 - }, - { - "LABEL": "2.6.25-linode9", - "ISXEN": 1, - "KERNELID": 88 - }, - { - "LABEL": "2.6.25.10-linode12", - "ISXEN": 1, - "KERNELID": 90 - }, - { - "LABEL": "2.6.26-linode13", - "ISXEN": 1, - "KERNELID": 91 - }, - { - "LABEL": "2.6.27.4-linode14", - "ISXEN": 1, - "KERNELID": 93 - }, - { - "LABEL": "2.6.28-linode15", - "ISXEN": 1, - "KERNELID": 96 - }, - { - "LABEL": "2.6.28.3-linode17", - "ISXEN": 1, - "KERNELID": 99 - }, - { - "LABEL": "2.6.29-linode18", - "ISXEN": 1, - "KERNELID": 101 - }, - { - "LABEL": "2.6.16.38-x86_64-linode2", - "ISXEN": 1, - "KERNELID": 85 - }, - { - "LABEL": "2.6.18.8-x86_64-linode1", - "ISXEN": 1, - "KERNELID": 86 - }, - { - "LABEL": "2.6.27.4-x86_64-linode3", - "ISXEN": 1, - "KERNELID": 94 - }, - { - "LABEL": "2.6.28-x86_64-linode4", - "ISXEN": 1, - "KERNELID": 97 - }, - { - "LABEL": "2.6.28.3-x86_64-linode5", - "ISXEN": 1, - "KERNELID": 100 - }, - { - "LABEL": "2.6.29-x86_64-linode6", - "ISXEN": 1, - "KERNELID": 102 - }, - { - "LABEL": "3.9.3-x86-linode52", - "ISXEN": 1, - "KERNELID": 137 - }, - { - "LABEL": "3.9.3-x86_64-linode33", - "ISXEN": 1, - "KERNELID": 138 - } - ] -} \ No newline at end of file diff --git a/libcloud/test/compute/fixtures/linode/_avail_linodeplans.json b/libcloud/test/compute/fixtures/linode/_avail_linodeplans.json deleted file mode 100644 index ac2488934f..0000000000 --- a/libcloud/test/compute/fixtures/linode/_avail_linodeplans.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [{ - "CORES": 1, - "PRICE": 10.00, - "RAM": 1024, - "XFER": 2000, - "PLANID": 1, - "LABEL": "Linode 1024", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 24, - "HOURLY": 0.0150 - }, { - "CORES": 2, - "PRICE": 20.00, - "RAM": 2048, - "XFER": 3000, - "PLANID": 2, - "LABEL": "Linode 2048", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 48, - "HOURLY": 0.0300 - }, { - "CORES": 4, - "PRICE": 40.00, - "RAM": 4096, - "XFER": 4000, - "PLANID": 4, - "LABEL": "Linode 4096", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 96, - "HOURLY": 0.0600 - }, { - "CORES": 6, - "PRICE": 80.00, - "RAM": 8192, - "XFER": 8000, - "PLANID": 6, - "LABEL": "Linode 8192", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 192, - "HOURLY": 0.1200 - }, { - "CORES": 8, - "PRICE": 160.00, - "RAM": 16384, - "XFER": 16000, - "PLANID": 7, - "LABEL": "Linode 16384", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 384, - "HOURLY": 0.2400 - }, { - "CORES": 12, - "PRICE": 320.00, - "RAM": 32768, - "XFER": 20000, - "PLANID": 8, - "LABEL": "Linode 32768", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 768, - "HOURLY": 0.4800 - }, { - "CORES": 16, - "PRICE": 480.00, - "RAM": 49152, - "XFER": 20000, - "PLANID": 9, - "LABEL": "Linode 49152", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 1152, - "HOURLY": 0.7200 - }, { - "CORES": 20, - "PRICE": 640.00, - "RAM": 65536, - "XFER": 20000, - "PLANID": 10, - "LABEL": "Linode 65536", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 1536, - "HOURLY": 0.9600 - }, { - "CORES": 20, - "PRICE": 960.00, - "RAM": 98304, - "XFER": 20000, - "PLANID": 12, - "LABEL": "Linode 98304", - "AVAIL": { - "3": 500, - "2": 500, - "7": 500, - "6": 500, - "4": 500, - "8": 500 - }, - "DISK": 1920, - "HOURLY": 1.4400 - }], - "ACTION": "avail.linodeplans" -} diff --git a/libcloud/test/compute/fixtures/linode/_batch.json b/libcloud/test/compute/fixtures/linode/_batch.json deleted file mode 100644 index 36e28f9050..0000000000 --- a/libcloud/test/compute/fixtures/linode/_batch.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "ERRORARRAY": [], - "DATA": [ - { - "IPADDRESSID": 5384, - "RDNS_NAME": "li22-54.members.linode.com", - "LINODEID": 8098, - "ISPUBLIC": 1, - "IPADDRESS": "66.228.43.47" - }, - { - "IPADDRESSID": 5575, - "RDNS_NAME": "li22-245.members.linode.com", - "LINODEID": 8098, - "ISPUBLIC": 1, - "IPADDRESS": "75.127.96.245" - } - ], - "ACTION": "linode.ip.list" - } -] diff --git a/libcloud/test/compute/fixtures/linode/_linode_disk_list.json b/libcloud/test/compute/fixtures/linode/_linode_disk_list.json deleted file mode 100644 index 8ec05a9f1d..0000000000 --- a/libcloud/test/compute/fixtures/linode/_linode_disk_list.json +++ /dev/null @@ -1,28 +0,0 @@ -{ -"ERRORARRAY":[], -"ACTION":"linode.disk.list", -"DATA":[ - { - "UPDATE_DT":"2009-06-30 13:19:00.0", - "DISKID":55319, - "LABEL":"test label", - "TYPE":"ext3", - "LINODEID":8098, - "ISREADONLY":0, - "STATUS":1, - "CREATE_DT":"2008-04-04 10:08:06.0", - "SIZE":4096 - }, - { - "UPDATE_DT":"2009-07-18 12:53:043.0", - "DISKID":55320, - "LABEL":"256M Swap Image", - "TYPE":"swap", - "LINODEID":8098, - "ISREADONLY":0, - "STATUS":1, - "CREATE_DT":"2008-04-04 10:08:06.0", - "SIZE":256 - } - ] -} diff --git a/libcloud/test/compute/fixtures/linode/_linode_ip_list.json b/libcloud/test/compute/fixtures/linode/_linode_ip_list.json deleted file mode 100644 index 33a969ca68..0000000000 --- a/libcloud/test/compute/fixtures/linode/_linode_ip_list.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "ACTION": "linode.ip.list", - "DATA": [ - { - "IPADDRESS": "66.228.43.47", - "IPADDRESSID": 5384, - "ISPUBLIC": 1, - "LINODEID": 8098, - "RDNS_NAME": "li22-54.members.linode.com" - }, - { - "IPADDRESS": "75.127.96.245", - "IPADDRESSID": 5575, - "ISPUBLIC": 1, - "LINODEID": 8098, - "RDNS_NAME": "li22-245.members.linode.com" - } - ], - "ERRORARRAY": [] -} diff --git a/libcloud/test/compute/fixtures/linode/_linode_list.json b/libcloud/test/compute/fixtures/linode/_linode_list.json deleted file mode 100644 index 345f7cadbb..0000000000 --- a/libcloud/test/compute/fixtures/linode/_linode_list.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "ALERT_CPU_ENABLED": 1, - "ALERT_BWIN_ENABLED": 1, - "ALERT_BWQUOTA_ENABLED": 1, - "BACKUPWINDOW": 0, - "ALERT_DISKIO_THRESHOLD": 1000, - "DISTRIBUTIONVENDOR": "Debian", - "WATCHDOG": 1, - "DATACENTERID": 6, - "STATUS": 1, - "ALERT_DISKIO_ENABLED": 1, - "CREATE_DT": "2012-05-04 19:31:30.0", - "TOTALHD": 49152, - "ALERT_BWQUOTA_THRESHOLD": 80, - "TOTALRAM": 2048, - "ALERT_BWIN_THRESHOLD": 5, - "LINODEID": 8098, - "ALERT_BWOUT_THRESHOLD": 5, - "ALERT_BWOUT_ENABLED": 1, - "BACKUPSENABLED": 1, - "ALERT_CPU_THRESHOLD": 90, - "PLANID": "2", - "BACKUPWEEKLYDAY": 0, - "LABEL": "api-node3", - "LPM_DISPLAYGROUP": "test", - "TOTALXFER": 3000 - } - ], - "ACTION": "linode.list" -} diff --git a/libcloud/test/compute/fixtures/linode_v4/get_node_billing_suspension.json b/libcloud/test/compute/fixtures/linode_v4/get_node_billing_suspension.json new file mode 100644 index 0000000000..475287913a --- /dev/null +++ b/libcloud/test/compute/fixtures/linode_v4/get_node_billing_suspension.json @@ -0,0 +1,42 @@ +{ + "id": 22344420, + "label": "test_2", + "group": "", + "status": "billing_suspension", + "created": "2020-12-07T14:58:19", + "updated": "2020-12-07T14:58:19", + "type": "g6-nanode-1", + "ipv4": [ + "212.71.239.24" + ], + "ipv6": "2a01:7e00::f03c:92ff:fe48:dc47/64", + "image": "linode/centos8", + "region": "eu-west", + "specs": { + "disk": 25600, + "memory": 1024, + "vcpus": 1, + "gpus": 0, + "transfer": 1000 + }, + "alerts": { + "cpu": 90, + "network_in": 10, + "network_out": 10, + "transfer_quota": 80, + "io": 10000 + }, + "backups": { + "enabled": false, + "schedule": { + "day": null, + "window": null + }, + "last_successful": null + }, + "hypervisor": "kvm", + "watchdog_enabled": true, + "tags": [ + "testing" + ] +} diff --git a/libcloud/test/compute/fixtures/linode_v4/list_nodes_extra_fields.json b/libcloud/test/compute/fixtures/linode_v4/list_nodes_extra_fields.json new file mode 100644 index 0000000000..5f65ce362f --- /dev/null +++ b/libcloud/test/compute/fixtures/linode_v4/list_nodes_extra_fields.json @@ -0,0 +1,49 @@ +{ + "data": [ + { + "id": 22344420, + "label": "test_2", + "group": "", + "status": "busy", + "created": "2020-10-08T18:51:29", + "updated": "2020-10-08T18:51:29", + "type": "g6-nanode-1", + "ipv4": ["138.89.34.81", "192.168.1.230"], + "ipv6": "2a01:7e00::f03c:92ff:fe48:dc47/64", + "image": "linode/centos7", + "region": "eu-west", + "specs": { + "disk": 25600, + "memory": 1024, + "vcpus": 1, + "gpus": 0, + "transfer": 1000 + }, + "alerts": { + "cpu": 90, + "network_in": 10, + "network_out": 10, + "transfer_quota": 80, + "io": 10000 + }, + "backups": { + "enabled": false, + "schedule": {"day": null, "window": null}, + "last_successful": null + }, + "capabilities": ["Block Storage Encryption"], + "disk_encryption": "enabled", + "has_user_data": true, + "hypervisor": "kvm", + "host_uuid": "3a3ddd59d9a78bb8de041391075df44de62bfec8", + "interface_generation": "linode", + "maintenance_policy": "linode/migrate", + "placement_group": {"id": 528, "label": "pg-1"}, + "watchdog_enabled": true, + "tags": ["testing"] + } + ], + "page": 1, + "pages": 1, + "results": 1 +} diff --git a/libcloud/test/compute/test_linode.py b/libcloud/test/compute/test_linode.py deleted file mode 100644 index bbe6354488..0000000000 --- a/libcloud/test/compute/test_linode.py +++ /dev/null @@ -1,208 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Maintainer: Jed Smith -# Based upon code written by Alex Polvi -# - -import sys -import unittest - -from libcloud.test import MockHttp -from libcloud.utils.py3 import httplib -from libcloud.compute.base import Node, StorageVolume, NodeAuthSSHKey, NodeAuthPassword -from libcloud.test.compute import TestCaseMixin -from libcloud.test.file_fixtures import ComputeFileFixtures -from libcloud.compute.drivers.linode import LinodeNodeDriver - - -class LinodeTest(unittest.TestCase, TestCaseMixin): - # The Linode test suite - - def setUp(self): - LinodeNodeDriver.connectionCls.conn_class = LinodeMockHttp - LinodeMockHttp.use_param = "api_action" - self.driver = LinodeNodeDriver("foo", api_version="3.0") - - def test_list_nodes(self): - nodes = self.driver.list_nodes() - self.assertEqual(len(nodes), 1) - node = nodes[0] - self.assertEqual(node.id, "8098") - self.assertEqual(node.name, "api-node3") - self.assertEqual(node.extra["PLANID"], "2") - self.assertTrue("75.127.96.245" in node.public_ips) - self.assertEqual(node.private_ips, []) - - def test_reboot_node(self): - # An exception would indicate failure - node = self.driver.list_nodes()[0] - self.driver.reboot_node(node) - - def test_destroy_node(self): - # An exception would indicate failure - node = self.driver.list_nodes()[0] - self.driver.destroy_node(node) - - def test_create_node_password_auth(self): - # Will exception on failure - self.driver.create_node( - name="Test", - location=self.driver.list_locations()[0], - size=self.driver.list_sizes()[0], - image=self.driver.list_images()[6], - auth=NodeAuthPassword("test123"), - ) - - def test_create_node_ssh_key_auth(self): - # Will exception on failure - node = self.driver.create_node( - name="Test", - location=self.driver.list_locations()[0], - size=self.driver.list_sizes()[0], - image=self.driver.list_images()[6], - auth=NodeAuthSSHKey("foo"), - ) - self.assertTrue(isinstance(node, Node)) - - def test_list_sizes(self): - sizes = self.driver.list_sizes() - self.assertEqual(len(sizes), 9) - for size in sizes: - self.assertEqual(size.ram, int(size.name.split(" ")[1])) - - def test_list_images(self): - images = self.driver.list_images() - self.assertEqual(len(images), 30) - - def test_create_node_response(self): - # should return a node object - node = self.driver.create_node( - name="node-name", - location=self.driver.list_locations()[0], - size=self.driver.list_sizes()[0], - image=self.driver.list_images()[0], - auth=NodeAuthPassword("foobar"), - ) - self.assertTrue(isinstance(node, Node)) - - def test_destroy_volume(self): - # Will exception on failure - node = self.driver.list_nodes()[0] - volume = StorageVolume( - id=55648, - name="test", - size=1024, - driver=self.driver, - extra={"LINODEID": node.id}, - ) - self.driver.destroy_volume(volume) - - def test_ex_create_volume(self): - # should return a StorageVolume object - node = self.driver.list_nodes()[0] - volume = self.driver.ex_create_volume( - size=4096, name="Another test image", node=node, fs_type="ext4" - ) - self.assertTrue(isinstance(volume, StorageVolume)) - - def test_ex_list_volumes(self): - # should return list of StorageVolume objects - node = self.driver.list_nodes()[0] - volumes = self.driver.ex_list_volumes(node=node) - - self.assertTrue(isinstance(volumes, list)) - self.assertTrue(isinstance(volumes[0], StorageVolume)) - self.assertEqual(len(volumes), 2) - - -class LinodeMockHttp(MockHttp): - fixtures = ComputeFileFixtures("linode") - - def _avail_datacenters(self, method, url, body, headers): - body = self.fixtures.load("_avail_datacenters.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _avail_linodeplans(self, method, url, body, headers): - body = self.fixtures.load("_avail_linodeplans.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _avail_distributions(self, method, url, body, headers): - body = self.fixtures.load("_avail_distributions.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_create(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.create","DATA":{"LinodeID":8098}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_disk_create(self, method, url, body, headers): - body = ( - '{"ERRORARRAY":[],"ACTION":"linode.disk.create","DATA":{"JobID":1298,"DiskID":55647}}' - ) - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_disk_delete(self, method, url, body, headers): - body = ( - '{"ERRORARRAY":[],"ACTION":"linode.disk.delete","DATA":{"JobID":1298,"DiskID":55648}}' - ) - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_disk_createfromdistribution(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.disk.createFromDistribution","DATA":{"JobID":1298,"DiskID":55647}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_disk_list(self, method, url, body, headers): - body = self.fixtures.load("_linode_disk_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_delete(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.delete","DATA":{"LinodeID":8098}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_update(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.update","DATA":{"LinodeID":8098}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_reboot(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.reboot","DATA":{"JobID":1305}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _avail_kernels(self, method, url, body, headers): - body = self.fixtures.load("_avail_kernels.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_boot(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.boot","DATA":{"JobID":1300}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_config_create(self, method, url, body, headers): - body = '{"ERRORARRAY":[],"ACTION":"linode.config.create","DATA":{"ConfigID":31239}}' - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_list(self, method, url, body, headers): - body = self.fixtures.load("_linode_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _linode_ip_list(self, method, url, body, headers): - body = self.fixtures.load("_linode_ip_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _batch(self, method, url, body, headers): - body = self.fixtures.load("_batch.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - -if __name__ == "__main__": - sys.exit(unittest.main()) diff --git a/libcloud/test/compute/test_linode_v4.py b/libcloud/test/compute/test_linode_v4.py index ed5d5d2e52..2bae27938a 100644 --- a/libcloud/test/compute/test_linode_v4.py +++ b/libcloud/test/compute/test_linode_v4.py @@ -47,6 +47,9 @@ def setUp(self): def test_unknown_api_version(self): self.assertRaises(NotImplementedError, LinodeNodeDriver, "foo", api_version="2.0") + def test_removed_api_version(self): + self.assertRaises(NotImplementedError, LinodeNodeDriver, "foo", api_version="3.0") + def test_correct_class_is_used(self): self.assertIsInstance(self.driver, LinodeNodeDriverV4) diff --git a/libcloud/test/dns/fixtures/linode/create_domain.json b/libcloud/test/dns/fixtures/linode/create_domain.json deleted file mode 100644 index a9eef97a03..0000000000 --- a/libcloud/test/dns/fixtures/linode/create_domain.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "ACTION": "domain.create", - "DATA": { - "DomainID": 5094 - } -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/create_domain_validation_error.json b/libcloud/test/dns/fixtures/linode/create_domain_validation_error.json deleted file mode 100644 index 3c7059724f..0000000000 --- a/libcloud/test/dns/fixtures/linode/create_domain_validation_error.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 8, - "ERRORMESSAGE": "The domain 'linode.com' already exists in our database. Please open a ticket if you think this is in error." - } - ], - "DATA": {}, - "ACTION": "domain.create" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/create_resource.json b/libcloud/test/dns/fixtures/linode/create_resource.json deleted file mode 100644 index 0fa3738e35..0000000000 --- a/libcloud/test/dns/fixtures/linode/create_resource.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": { - "ResourceID": 3585100 - }, - "ACTION": "domain.resource.create" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/delete_domain.json b/libcloud/test/dns/fixtures/linode/delete_domain.json deleted file mode 100644 index ff39a38ed7..0000000000 --- a/libcloud/test/dns/fixtures/linode/delete_domain.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "ACTION": "domain.delete", - "DATA": { - "DomainID": 5123 - } -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/delete_domain_does_not_exist.json b/libcloud/test/dns/fixtures/linode/delete_domain_does_not_exist.json deleted file mode 100644 index 8965baa6ee..0000000000 --- a/libcloud/test/dns/fixtures/linode/delete_domain_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.delete" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/delete_resource.json b/libcloud/test/dns/fixtures/linode/delete_resource.json deleted file mode 100644 index 7f7af2adbf..0000000000 --- a/libcloud/test/dns/fixtures/linode/delete_resource.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": { - "ResourceID": 3585141 - }, - "ACTION": "domain.resource.delete" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/delete_resource_does_not_exist.json b/libcloud/test/dns/fixtures/linode/delete_resource_does_not_exist.json deleted file mode 100644 index b6969aa64c..0000000000 --- a/libcloud/test/dns/fixtures/linode/delete_resource_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.resource.delete" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/domain_list.json b/libcloud/test/dns/fixtures/linode/domain_list.json deleted file mode 100644 index ac88b9b5ef..0000000000 --- a/libcloud/test/dns/fixtures/linode/domain_list.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "ERRORARRAY": [], - "ACTION": "domain.list", - "DATA": [ - { - "DOMAINID": 5093, - "DESCRIPTION": "", - "EXPIRE_SEC": 0, - "RETRY_SEC": 0, - "STATUS": 1, - "LPM_DISPLAYGROUP": "thing", - "MASTER_IPS": "", - "REFRESH_SEC": 0, - "SOA_EMAIL": "dns@example.com", - "TTL_SEC": 0, - "DOMAIN": "linode.com", - "AXFR_IPS": "none", - "TYPE": "master" - }, - { - "DOMAINID": 5094, - "DESCRIPTION": "", - "EXPIRE_SEC": 0, - "RETRY_SEC": 0, - "STATUS": 1, - "LPM_DISPLAYGROUP": "", - "MASTER_IPS": "2600:3c03::f03c:91ff:feae:e071;66.228.43.47;", - "REFRESH_SEC": 0, - "SOA_EMAIL": "", - "TTL_SEC": 0, - "DOMAIN": "0.c.d.7.0.6.0.f.1.0.7.4.0.1.0.0.2.ip6.arpa", - "AXFR_IPS": "2600:3c03::f03c:91ff:feae:e071;66.228.43.47;", - "TYPE": "slave" - } - ] -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/get_record.json b/libcloud/test/dns/fixtures/linode/get_record.json deleted file mode 100644 index 4d5b0eb597..0000000000 --- a/libcloud/test/dns/fixtures/linode/get_record.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "DOMAINID": 5093, - "PORT": 80, - "RESOURCEID": 3585100, - "NAME": "www", - "WEIGHT": 5, - "TTL_SEC": 0, - "TARGET": "127.0.0.1", - "PRIORITY": 10, - "PROTOCOL": "", - "TYPE": "a" - } - ], - "ACTION": "domain.resource.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/get_record_does_not_exist.json b/libcloud/test/dns/fixtures/linode/get_record_does_not_exist.json deleted file mode 100644 index c2c1fb499d..0000000000 --- a/libcloud/test/dns/fixtures/linode/get_record_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.resource.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/get_zone.json b/libcloud/test/dns/fixtures/linode/get_zone.json deleted file mode 100644 index e9387122f6..0000000000 --- a/libcloud/test/dns/fixtures/linode/get_zone.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "DOMAINID": 5093, - "DESCRIPTION": "", - "EXPIRE_SEC": 0, - "RETRY_SEC": 0, - "STATUS": 1, - "LPM_DISPLAYGROUP": "thing", - "MASTER_IPS": "", - "REFRESH_SEC": 0, - "SOA_EMAIL": "dns@example.com", - "TTL_SEC": 0, - "DOMAIN": "linode.com", - "AXFR_IPS": "none", - "TYPE": "master" - } - ], - "ACTION": "domain.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/get_zone_does_not_exist.json b/libcloud/test/dns/fixtures/linode/get_zone_does_not_exist.json deleted file mode 100644 index ea18547f51..0000000000 --- a/libcloud/test/dns/fixtures/linode/get_zone_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/resource_list.json b/libcloud/test/dns/fixtures/linode/resource_list.json deleted file mode 100644 index 2ed18cd821..0000000000 --- a/libcloud/test/dns/fixtures/linode/resource_list.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": [ - { - "DOMAINID": 5093, - "PORT": 80, - "RESOURCEID": 3585100, - "NAME": "mc", - "WEIGHT": 5, - "TTL_SEC": 0, - "TARGET": "127.0.0.1", - "PRIORITY": 10, - "PROTOCOL": "", - "TYPE": "a" - }, - { - "DOMAINID": 5093, - "PORT": 25565, - "RESOURCEID": 3585141, - "NAME": "_minecraft._udp", - "WEIGHT": 5, - "TTL_SEC": 0, - "TARGET": "mc.linode.com", - "PRIORITY": 10, - "PROTOCOL": "udp", - "TYPE": "srv" - } - ], - "ACTION": "domain.resource.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/resource_list_does_not_exist.json b/libcloud/test/dns/fixtures/linode/resource_list_does_not_exist.json deleted file mode 100644 index c2c1fb499d..0000000000 --- a/libcloud/test/dns/fixtures/linode/resource_list_does_not_exist.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ERRORARRAY": [ - { - "ERRORCODE": 5, - "ERRORMESSAGE": "Object not found" - } - ], - "DATA": {}, - "ACTION": "domain.resource.list" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/update_domain.json b/libcloud/test/dns/fixtures/linode/update_domain.json deleted file mode 100644 index f695f4c103..0000000000 --- a/libcloud/test/dns/fixtures/linode/update_domain.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": { - "DomainID": 5093 - }, - "ACTION": "domain.update" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode/update_resource.json b/libcloud/test/dns/fixtures/linode/update_resource.json deleted file mode 100644 index 7f88aabe6f..0000000000 --- a/libcloud/test/dns/fixtures/linode/update_resource.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ERRORARRAY": [], - "DATA": { - "ResourceID": 3585100 - }, - "ACTION": "domain.resource.update" -} \ No newline at end of file diff --git a/libcloud/test/dns/fixtures/linode_v4/create_record_ptr.json b/libcloud/test/dns/fixtures/linode_v4/create_record_ptr.json new file mode 100644 index 0000000000..23f63d2905 --- /dev/null +++ b/libcloud/test/dns/fixtures/linode_v4/create_record_ptr.json @@ -0,0 +1,15 @@ +{ + "id": 123, + "type": "PTR", + "name": "10", + "target": "host.example.com", + "priority": 0, + "weight": 0, + "port": 0, + "service": null, + "protocol": null, + "ttl_sec": 300, + "tag": null, + "created": "2020-10-16T07:57:37", + "updated": "2020-10-16T07:57:37" +} diff --git a/libcloud/test/dns/fixtures/linode_v4/create_zone_axfr_ips.json b/libcloud/test/dns/fixtures/linode_v4/create_zone_axfr_ips.json new file mode 100644 index 0000000000..8e55554cdc --- /dev/null +++ b/libcloud/test/dns/fixtures/linode_v4/create_zone_axfr_ips.json @@ -0,0 +1,21 @@ +{ + "id": 123, + "type": "master", + "domain": "example.com", + "tags": [], + "group": "", + "status": "active", + "description": "", + "soa_email": "admin@example.com", + "retry_sec": 0, + "master_ips": [], + "axfr_ips": [ + "192.0.2.10", + "198.51.100.11" + ], + "expire_sec": 0, + "refresh_sec": 0, + "ttl_sec": 300, + "created": "2020-10-16T09:08:50", + "updated": "2020-10-16T09:08:50" +} diff --git a/libcloud/test/dns/test_linode.py b/libcloud/test/dns/test_linode.py deleted file mode 100644 index 7e6b3439b2..0000000000 --- a/libcloud/test/dns/test_linode.py +++ /dev/null @@ -1,316 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and - -import sys -import unittest - -from libcloud.test import MockHttp -from libcloud.dns.types import RecordType, ZoneDoesNotExistError, RecordDoesNotExistError -from libcloud.utils.py3 import httplib -from libcloud.test.secrets import DNS_PARAMS_LINODE, DNS_KEYWORD_PARAMS_LINODE -from libcloud.common.linode import LinodeException -from libcloud.dns.drivers.linode import LinodeDNSDriver, LinodeDNSDriverV3 -from libcloud.test.file_fixtures import DNSFileFixtures - - -class LinodeTests(unittest.TestCase): - def setUp(self): - LinodeDNSDriverV3.connectionCls.conn_class = LinodeMockHttp - LinodeMockHttp.use_param = "api_action" - LinodeMockHttp.type = None - self.driver = LinodeDNSDriver(*DNS_PARAMS_LINODE, **DNS_KEYWORD_PARAMS_LINODE) - - def assertHasKeys(self, dictionary, keys): - for key in keys: - self.assertTrue(key in dictionary, 'key "%s" not in dictionary' % (key)) - - def test_list_record_types(self): - record_types = self.driver.list_record_types() - self.assertEqual(len(record_types), 7) - self.assertTrue(RecordType.A in record_types) - - def test_list_zones_success(self): - zones = self.driver.list_zones() - self.assertEqual(len(zones), 2) - - zone = zones[0] - self.assertEqual(zone.id, "5093") - self.assertEqual(zone.type, "master") - self.assertEqual(zone.domain, "linode.com") - self.assertIsNone(zone.ttl) - self.assertHasKeys(zone.extra, ["description", "SOA_Email", "status"]) - - def test_list_records_success(self): - zone = self.driver.list_zones()[0] - records = self.driver.list_records(zone=zone) - self.assertEqual(len(records), 2) - - arecord = records[0] - self.assertEqual(arecord.id, "3585100") - self.assertEqual(arecord.name, "mc") - self.assertEqual(arecord.type, RecordType.A) - self.assertEqual(arecord.data, "127.0.0.1") - self.assertHasKeys(arecord.extra, ["protocol", "ttl_sec", "port", "weight"]) - - srvrecord = records[1] - self.assertEqual(srvrecord.id, "3585141") - self.assertEqual(srvrecord.name, "_minecraft._udp") - self.assertEqual(srvrecord.type, RecordType.SRV) - self.assertEqual(srvrecord.data, "mc.linode.com") - self.assertHasKeys(srvrecord.extra, ["protocol", "ttl_sec", "port", "priority", "weight"]) - - def test_list_records_zone_does_not_exist(self): - zone = self.driver.list_zones()[0] - - LinodeMockHttp.type = "ZONE_DOES_NOT_EXIST" - try: - self.driver.list_records(zone=zone) - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, zone.id) - else: - self.fail("Exception was not thrown") - - def test_get_zone_success(self): - LinodeMockHttp.type = "GET_ZONE" - - zone = self.driver.get_zone(zone_id="5093") - self.assertEqual(zone.id, "5093") - self.assertEqual(zone.type, "master") - self.assertEqual(zone.domain, "linode.com") - self.assertIsNone(zone.ttl) - self.assertHasKeys(zone.extra, ["description", "SOA_Email", "status"]) - - def test_get_zone_does_not_exist(self): - LinodeMockHttp.type = "GET_ZONE_DOES_NOT_EXIST" - - try: - self.driver.get_zone(zone_id="4444") - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, "4444") - else: - self.fail("Exception was not thrown") - - def test_get_record_success(self): - LinodeMockHttp.type = "GET_RECORD" - record = self.driver.get_record(zone_id="1234", record_id="3585100") - self.assertEqual(record.id, "3585100") - self.assertEqual(record.name, "www") - self.assertEqual(record.type, RecordType.A) - self.assertEqual(record.data, "127.0.0.1") - self.assertHasKeys(record.extra, ["protocol", "ttl_sec", "port", "weight"]) - - def test_get_record_zone_does_not_exist(self): - LinodeMockHttp.type = "GET_RECORD_ZONE_DOES_NOT_EXIST" - - try: - self.driver.get_record(zone_id="444", record_id="3585100") - except ZoneDoesNotExistError: - pass - else: - self.fail("Exception was not thrown") - - def test_get_record_record_does_not_exist(self): - LinodeMockHttp.type = "GET_RECORD_RECORD_DOES_NOT_EXIST" - - try: - self.driver.get_record(zone_id="4441", record_id="3585100") - except RecordDoesNotExistError: - pass - else: - self.fail("Exception was not thrown") - - def test_create_zone_success(self): - zone = self.driver.create_zone(domain="foo.bar.com", type="master", ttl=None, extra=None) - self.assertEqual(zone.id, "5094") - self.assertEqual(zone.domain, "foo.bar.com") - - def test_create_zone_validaton_error(self): - LinodeMockHttp.type = "VALIDATION_ERROR" - - try: - self.driver.create_zone(domain="foo.bar.com", type="master", ttl=None, extra=None) - except LinodeException: - pass - else: - self.fail("Exception was not thrown") - - def test_update_zone_success(self): - zone = self.driver.list_zones()[0] - updated_zone = self.driver.update_zone( - zone=zone, - domain="libcloud.org", - ttl=10, - extra={"SOA_Email": "bar@libcloud.org"}, - ) - - self.assertEqual(zone.extra["SOA_Email"], "dns@example.com") - - self.assertEqual(updated_zone.id, zone.id) - self.assertEqual(updated_zone.domain, "libcloud.org") - self.assertEqual(updated_zone.type, zone.type) - self.assertEqual(updated_zone.ttl, 10) - self.assertEqual(updated_zone.extra["SOA_Email"], "bar@libcloud.org") - self.assertEqual(updated_zone.extra["status"], zone.extra["status"]) - self.assertEqual(updated_zone.extra["description"], zone.extra["description"]) - - def test_create_record_success(self): - zone = self.driver.list_zones()[0] - record = self.driver.create_record( - name="www", zone=zone, type=RecordType.A, data="127.0.0.1" - ) - - self.assertEqual(record.id, "3585100") - self.assertEqual(record.name, "www") - self.assertEqual(record.zone, zone) - self.assertEqual(record.type, RecordType.A) - self.assertEqual(record.data, "127.0.0.1") - - def test_update_record_success(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - updated_record = self.driver.update_record( - record=record, name="www", type=RecordType.AAAA, data="::1" - ) - - self.assertEqual(record.data, "127.0.0.1") - - self.assertEqual(updated_record.id, record.id) - self.assertEqual(updated_record.name, "www") - self.assertEqual(updated_record.zone, record.zone) - self.assertEqual(updated_record.type, RecordType.AAAA) - self.assertEqual(updated_record.data, "::1") - - def test_delete_zone_success(self): - zone = self.driver.list_zones()[0] - status = self.driver.delete_zone(zone=zone) - self.assertTrue(status) - - def test_delete_zone_does_not_exist(self): - zone = self.driver.list_zones()[0] - - LinodeMockHttp.type = "ZONE_DOES_NOT_EXIST" - - try: - self.driver.delete_zone(zone=zone) - except ZoneDoesNotExistError as e: - self.assertEqual(e.zone_id, zone.id) - else: - self.fail("Exception was not thrown") - - def test_delete_record_success(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - status = self.driver.delete_record(record=record) - self.assertTrue(status) - - def test_delete_record_does_not_exist(self): - zone = self.driver.list_zones()[0] - record = self.driver.list_records(zone=zone)[0] - - LinodeMockHttp.type = "RECORD_DOES_NOT_EXIST" - - try: - self.driver.delete_record(record=record) - except RecordDoesNotExistError as e: - self.assertEqual(e.record_id, record.id) - else: - self.fail("Exception was not thrown") - - -class LinodeMockHttp(MockHttp): - fixtures = DNSFileFixtures("linode") - - def _domain_list(self, method, url, body, headers): - body = self.fixtures.load("domain_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("resource_list.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _ZONE_DOES_NOT_EXIST_domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("resource_list_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_ZONE_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_ZONE_DOES_NOT_EXIST_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("get_record.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_ZONE_DOES_NOT_EXIST_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_ZONE_DOES_NOT_EXIST_domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("get_record_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_RECORD_DOES_NOT_EXIST_domain_list(self, method, url, body, headers): - body = self.fixtures.load("get_zone.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _GET_RECORD_RECORD_DOES_NOT_EXIST_domain_resource_list(self, method, url, body, headers): - body = self.fixtures.load("get_record_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_create(self, method, url, body, headers): - body = self.fixtures.load("create_domain.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _VALIDATION_ERROR_domain_create(self, method, url, body, headers): - body = self.fixtures.load("create_domain_validation_error.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_update(self, method, url, body, headers): - body = self.fixtures.load("update_domain.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_resource_create(self, method, url, body, headers): - body = self.fixtures.load("create_resource.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_resource_update(self, method, url, body, headers): - body = self.fixtures.load("update_resource.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_delete(self, method, url, body, headers): - body = self.fixtures.load("delete_domain.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _ZONE_DOES_NOT_EXIST_domain_delete(self, method, url, body, headers): - body = self.fixtures.load("delete_domain_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _domain_resource_delete(self, method, url, body, headers): - body = self.fixtures.load("delete_resource.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _RECORD_DOES_NOT_EXIST_domain_resource_delete(self, method, url, body, headers): - body = self.fixtures.load("delete_resource_does_not_exist.json") - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - -if __name__ == "__main__": - sys.exit(unittest.main()) diff --git a/libcloud/test/dns/test_linode_v4.py b/libcloud/test/dns/test_linode_v4.py index 3b461f74cd..476c876cfa 100644 --- a/libcloud/test/dns/test_linode_v4.py +++ b/libcloud/test/dns/test_linode_v4.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and import sys +import json import unittest from libcloud.test import MockHttp @@ -35,6 +36,9 @@ def test_correct_class_is_used(self): def test_unknown_api_version(self): self.assertRaises(NotImplementedError, LinodeDNSDriver, "foo", api_version="2.0") + def test_removed_api_version(self): + self.assertRaises(NotImplementedError, LinodeDNSDriver, "foo", api_version="3.0") + def test_list_zones(self): zones = self.driver.list_zones() self.assertEqual(len(zones), 3) @@ -78,6 +82,12 @@ def test_get_record_MX_RECORD(self): self.assertEqual(record.data, "mail.example.com") self.assertEqual(record.type, "MX") + def test_list_records_preserves_CAA_tag(self): + zone = self.driver.list_zones()[0] + records = self.driver.list_records(zone) + caa_record = next(record for record in records if record.type == RecordType.CAA) + self.assertEqual(caa_record.extra["tag"], "issue") + def test_create_zone(self): domain = "example.com" ttl = 300 @@ -87,6 +97,17 @@ def test_create_zone(self): self.assertEqual(zone.domain, "example.com") self.assertEqual(zone.extra["soa_email"], "admin@example.com") + def test_create_zone_accepts_axfr_ips(self): + LinodeMockHttpV4.type = "CREATE_ZONE_AXFR_IPS" + domain = "example.com" + ttl = 300 + extra = { + "soa_email": "admin@example.com", + "axfr_ips": ["192.0.2.10", "198.51.100.11"], + } + zone = self.driver.create_zone(domain=domain, ttl=ttl, extra=extra) + self.assertEqual(zone.extra["axfr_ips"], extra["axfr_ips"]) + def test_create_record(self): zone = self.driver.list_zones()[0] name = "test" @@ -98,6 +119,13 @@ def test_create_record(self): self.assertEqual(record.type, "A") self.assertEqual(record.data, data) + def test_create_record_PTR_RECORD(self): + zone = self.driver.list_zones()[0] + LinodeMockHttpV4.type = "PTR_RECORD" + record = self.driver.create_record("10", zone, RecordType.PTR, "host.example.com") + self.assertEqual(record.type, "PTR") + self.assertEqual(record.data, "host.example.com") + def test_update_zone(self): zone = self.driver.list_zones()[0] domain = "example.com" @@ -141,6 +169,12 @@ def _v4_domains(self, method, url, body, headers): body = self.fixtures.load("create_zone.json") return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _v4_domains_CREATE_ZONE_AXFR_IPS(self, method, url, body, headers): + payload = json.loads(body) + self.assertEqual(payload["axfr_ips"], ["192.0.2.10", "198.51.100.11"]) + body = self.fixtures.load("create_zone_axfr_ips.json") + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _v4_domains_123_records(self, method, url, body, headers): if method == "GET": body = self.fixtures.load("list_records.json") @@ -149,6 +183,13 @@ def _v4_domains_123_records(self, method, url, body, headers): body = self.fixtures.load("create_record.json") return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _v4_domains_123_records_PTR_RECORD(self, method, url, body, headers): + payload = json.loads(body) + self.assertEqual(payload["type"], "PTR") + self.assertEqual(payload["target"], "host.example.com") + body = self.fixtures.load("create_record_ptr.json") + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _v4_domains_123(self, method, url, body, headers): if method == "GET": body = self.fixtures.load("get_zone.json")