| # 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. |
| |
| """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(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 |
| from libcloud.compute.base import ( |
| Node, |
| NodeSize, |
| NodeImage, |
| 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, |
| ) |
| from libcloud.compute.types import Provider, NodeState, StorageVolumeState |
| from libcloud.utils.networking import is_private_subnet |
| |
| try: |
| import simplejson as json |
| except ImportError: |
| import json |
| |
| |
| class LinodeNodeDriver(NodeDriver): |
| name = "Linode" |
| website = "http://www.linode.com/" |
| type = Provider.LINODE |
| |
| def __new__( |
| cls, |
| key, |
| secret=None, |
| secure=True, |
| host=None, |
| port=None, |
| api_version=DEFAULT_API_VERSION, |
| region=None, |
| **kwargs, |
| ): |
| if cls is LinodeNodeDriver: |
| if api_version == "3.0": |
| cls = LinodeNodeDriverV3 |
| elif api_version == "4.0": |
| cls = LinodeNodeDriverV4 |
| else: |
| raise NotImplementedError( |
| "No Linode driver found for API version: %s" % (api_version) |
| ) |
| 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 <https://www.libcloud.org>" |
| 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"] whithin :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): |
| """ |
| Covert 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 |
| |
| LINODE_STATES = { |
| "running": NodeState.RUNNING, |
| "stopped": NodeState.STOPPED, |
| "provisioning": NodeState.STARTING, |
| "offline": NodeState.STOPPED, |
| "booting": NodeState.STARTING, |
| "rebooting": NodeState.REBOOTING, |
| "shutting_down": NodeState.STOPPING, |
| "deleting": NodeState.PENDING, |
| "migrating": NodeState.MIGRATING, |
| "rebuilding": NodeState.UPDATING, |
| "cloning": NodeState.MIGRATING, |
| "restoring": NodeState.PENDING, |
| "resizing": NodeState.RECONFIGURING, |
| } |
| |
| LINODE_DISK_STATES = { |
| "ready": StorageVolumeState.AVAILABLE, |
| "not ready": StorageVolumeState.CREATING, |
| "deleting": StorageVolumeState.DELETING, |
| } |
| |
| LINODE_VOLUME_STATES = { |
| "creating": StorageVolumeState.CREATING, |
| "active": StorageVolumeState.AVAILABLE, |
| "resizing": StorageVolumeState.UPDATING, |
| "contact_support": StorageVolumeState.UNKNOWN, |
| } |
| |
| def list_nodes(self): |
| """ |
| Returns a list of Linodes the API key in use has access |
| to view. |
| |
| :return: List of node objects |
| :rtype: ``list`` of :class:`Node` |
| """ |
| |
| data = self._paginated_request("/v4/linode/instances", "data") |
| return [self._to_node(obj) for obj in data] |
| |
| def list_sizes(self): |
| """ |
| Returns a list of Linode Types |
| |
| : rtype: ``list`` of :class: `NodeSize` |
| """ |
| data = self._paginated_request("/v4/linode/types", "data") |
| return [self._to_size(obj) for obj in data] |
| |
| def list_images(self): |
| """ |
| Returns a list of images |
| |
| :rtype: ``list`` of :class:`NodeImage` |
| """ |
| data = self._paginated_request("/v4/images", "data") |
| return [self._to_image(obj) for obj in data] |
| |
| def list_locations(self): |
| """ |
| Lists the Regions available for Linode services |
| |
| :rtype: ``list`` of :class:`NodeLocation` |
| """ |
| data = self._paginated_request("/v4/regions", "data") |
| return [self._to_location(obj) for obj in data] |
| |
| def start_node(self, node): |
| """Boots a node the API Key has permission to modify |
| |
| :param node: the node to start |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| response = self.connection.request("/v4/linode/instances/%s/boot" % node.id, method="POST") |
| return response.status == httplib.OK |
| |
| 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 stop_node(self, node): |
| """Shuts down a a node the API Key has permission to modify. |
| |
| :param node: the Linode to destroy |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| response = self.connection.request( |
| "/v4/linode/instances/%s/shutdown" % node.id, method="POST" |
| ) |
| return response.status == httplib.OK |
| |
| 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 destroy_node(self, node): |
| """Deletes a node the API Key has permission to `read_write` |
| |
| :param node: the Linode to destroy |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| response = self.connection.request("/v4/linode/instances/%s" % node.id, method="DELETE") |
| return response.status == httplib.OK |
| |
| def reboot_node(self, node): |
| """Reboots a node the API Key has permission to modify. |
| |
| :param node: the Linode to destroy |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| response = self.connection.request( |
| "/v4/linode/instances/%s/reboot" % node.id, method="POST" |
| ) |
| return response.status == httplib.OK |
| |
| def create_node( |
| self, |
| location, |
| size, |
| image=None, |
| name=None, |
| root_pass=None, |
| ex_authorized_keys=None, |
| ex_authorized_users=None, |
| ex_tags=None, |
| ex_backups_enabled=False, |
| ex_private_ip=False, |
| ): |
| """Creates a Linode Instance. |
| In order for this request to complete successfully, |
| the user must have the `add_linodes` grant as this call |
| will incur a charge. |
| |
| :param location: which region to create the node in |
| :type location: :class:`NodeLocation` |
| |
| :param size: the plan size to create |
| :type size: :class:`NodeSize` |
| |
| :keyword image: which distribution to deploy on the node |
| :type image: :class:`NodeImage` |
| |
| :keyword name: the name to assign to node.\ |
| Must start with an alpha character.\ |
| May only consist of alphanumeric characters,\ |
| dashes (-), underscores (_) or periods (.).\ |
| Cannot have two dashes (--), underscores (__) or periods (..) in a row. |
| :type name: ``str`` |
| |
| :keyword root_pass: the root password (required if image is provided) |
| :type root_pass: ``str`` |
| |
| :keyword ex_authorized_keys: a list of public SSH keys |
| :type ex_authorized_keys: ``list`` of ``str`` |
| |
| :keyword ex_authorized_users: a list of usernames.\ |
| If the usernames have associated SSH keys,\ |
| the keys will be appended to the root users `authorized_keys` |
| :type ex_authorized_users: ``list`` of ``str`` |
| |
| :keyword ex_tags: list of tags for the node |
| :type ex_tags: ``list`` of ``str`` |
| |
| :keyword ex_backups_enabled: whether to be enrolled \ |
| in the Linode Backup service (False) |
| :type ex_backups_enabled: ``bool`` |
| |
| :keyword ex_private_ip: whether or not to request a private IP |
| :type ex_private_ip: ``bool`` |
| |
| :return: Node representing the newly-created node |
| :rtype: :class:`Node` |
| """ |
| |
| if not isinstance(location, NodeLocation): |
| raise LinodeExceptionV4("Invalid location instance") |
| |
| if not isinstance(size, NodeSize): |
| raise LinodeExceptionV4("Invalid size instance") |
| |
| attr = { |
| "region": location.id, |
| "type": size.id, |
| "private_ip": ex_private_ip, |
| "backups_enabled": ex_backups_enabled, |
| } |
| |
| if image is not None: |
| if root_pass is None: |
| raise LinodeExceptionV4("root password required " "when providing an image") |
| attr["image"] = image.id |
| attr["root_pass"] = root_pass |
| |
| if name is not None: |
| valid_name = r"^[a-zA-Z]((?!--|__|\.\.)[a-zA-Z0-9-_.])+$" |
| if not re.match(valid_name, name): |
| raise LinodeExceptionV4("Invalid name") |
| attr["label"] = name |
| if ex_authorized_keys is not None: |
| attr["authorized_keys"] = list(ex_authorized_keys) |
| if ex_authorized_users is not None: |
| attr["authorized_users"] = list(ex_authorized_users) |
| if ex_tags is not None: |
| attr["tags"] = list(ex_tags) |
| |
| response = self.connection.request( |
| "/v4/linode/instances", data=json.dumps(attr), method="POST" |
| ).object |
| return self._to_node(response) |
| |
| def ex_get_node(self, node_id): |
| """ |
| Return a Node object based on a node ID. |
| |
| :keyword node_id: Node's ID |
| :type node_id: ``str`` |
| |
| :return: Created node |
| :rtype : :class:`Node` |
| """ |
| response = self.connection.request("/v4/linode/instances/%s" % node_id).object |
| return self._to_node(response) |
| |
| def ex_list_disks(self, node): |
| """ |
| List disks associated with the node. |
| |
| :param node: Node to list disks. (required) |
| :type node: :class:`Node` |
| |
| :rtype: ``list`` of :class:`LinodeDisk` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| data = self._paginated_request("/v4/linode/instances/%s/disks" % node.id, "data") |
| |
| return [self._to_disk(obj) for obj in data] |
| |
| def ex_create_disk( |
| self, |
| size, |
| name, |
| node, |
| fs_type, |
| image=None, |
| ex_root_pass=None, |
| ex_authorized_keys=None, |
| ex_authorized_users=None, |
| ex_read_only=False, |
| ): |
| """ |
| Adds a new disk to node |
| |
| :param size: Size of disk in megabytes (required) |
| :type size: ``int`` |
| |
| :param name: Name of the disk to be created (required) |
| :type name: ``str`` |
| |
| :param node: Node to attach disk to (required) |
| :type node: :class:`Node` |
| |
| :param fs_type: The formatted type of this disk. Valid types are: |
| ext3, ext4, swap, raw, initrd |
| :type fs_type: ``str`` |
| |
| :keyword image: Image to deploy the volume from |
| :type image: :class:`NodeImage` |
| |
| :keyword ex_root_pass: root password,required \ |
| if an image is provided |
| :type ex_root_pass: ``str`` |
| |
| :keyword ex_authorized_keys: a list of SSH keys |
| :type ex_authorized_keys: ``list`` of ``str`` |
| |
| :keyword ex_authorized_users: a list of usernames \ |
| that will have their SSH keys,\ |
| if any, automatically appended \ |
| to the root user's ~/.ssh/authorized_keys file. |
| :type ex_authorized_users: ``list`` of ``str`` |
| |
| :keyword ex_read_only: if true, this disk is read-only |
| :type ex_read_only: ``bool`` |
| |
| :return: LinodeDisk representing the newly-created disk |
| :rtype: :class:`LinodeDisk` |
| """ |
| |
| attr = { |
| "label": str(name), |
| "size": int(size), |
| "filesystem": fs_type, |
| "read_only": ex_read_only, |
| } |
| |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| if fs_type not in self._linode_disk_filesystems: |
| raise LinodeExceptionV4("Not valid filesystem type") |
| |
| if image is not None: |
| if not isinstance(image, NodeImage): |
| raise LinodeExceptionV4("Invalid image instance") |
| # when an image is set, root pass must be set as well |
| if ex_root_pass is None: |
| raise LinodeExceptionV4("root_pass is required when " "deploying an image") |
| attr["image"] = image.id |
| attr["root_pass"] = ex_root_pass |
| |
| if ex_authorized_keys is not None: |
| attr["authorized_keys"] = list(ex_authorized_keys) |
| |
| if ex_authorized_users is not None: |
| attr["authorized_users"] = list(ex_authorized_users) |
| |
| response = self.connection.request( |
| "/v4/linode/instances/%s/disks" % node.id, |
| data=json.dumps(attr), |
| method="POST", |
| ).object |
| return self._to_disk(response) |
| |
| def ex_destroy_disk(self, node, disk): |
| """ |
| Destroys disk for the given node. |
| |
| :param node: The Node the disk is attached to. (required) |
| :type node: :class:`Node` |
| |
| :param disk: LinodeDisk to be destroyed (required) |
| :type disk: :class:`LinodeDisk` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| if not isinstance(disk, LinodeDisk): |
| raise LinodeExceptionV4("Invalid disk instance") |
| |
| if node.state != self.LINODE_STATES["stopped"]: |
| raise LinodeExceptionV4("Node needs to be stopped" " before disk is destroyed") |
| |
| response = self.connection.request( |
| "/v4/linode/instances/{}/disks/{}".format(node.id, disk.id), method="DELETE" |
| ) |
| return response.status == httplib.OK |
| |
| def list_volumes(self): |
| """Get all volumes of the account |
| :rtype: `list` of :class: `StorageVolume` |
| """ |
| data = self._paginated_request("/v4/volumes", "data") |
| |
| return [self._to_volume(obj) for obj in data] |
| |
| def create_volume(self, name, size, location=None, node=None, tags=None): |
| """Creates a volume and optionally attaches it to a node. |
| |
| :param name: The name to be given to volume (required).\ |
| Must start with an alpha character. \ |
| May only consist of alphanumeric characters,\ |
| dashes (-), underscores (_)\ |
| Cannot have two dashes (--), underscores (__) in a row. |
| |
| :type name: `str` |
| |
| :param size: Size in gigabytes (required) |
| :type size: `int` |
| |
| :keyword location: Location to create the node.\ |
| Required if node is not given. |
| :type location: :class:`NodeLocation` |
| |
| :keyword volume: Node to attach the volume to |
| :type volume: :class:`Node` |
| |
| :keyword tags: tags to apply to volume |
| :type tags: `list` of `str` |
| |
| :rtype: :class: `StorageVolume` |
| """ |
| |
| valid_name = "^[a-zA-Z]((?!--|__)[a-zA-Z0-9-_])+$" |
| if not re.match(valid_name, name): |
| raise LinodeExceptionV4("Invalid name") |
| |
| attr = { |
| "label": name, |
| "size": int(size), |
| } |
| |
| if node is not None: |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| attr["linode_id"] = int(node.id) |
| else: |
| # location is only required if a node is not given |
| if location: |
| if not isinstance(location, NodeLocation): |
| raise LinodeExceptionV4("Invalid location instance") |
| attr["region"] = location.id |
| else: |
| raise LinodeExceptionV4("Region must be provided " "when node is not") |
| if tags is not None: |
| attr["tags"] = list(tags) |
| |
| response = self.connection.request( |
| "/v4/volumes", data=json.dumps(attr), method="POST" |
| ).object |
| return self._to_volume(response) |
| |
| def attach_volume(self, node, volume, persist_across_boots=True): |
| """Attaches a volume to a node. |
| Volume and node must be located in the same region |
| |
| :param node: Node to attach the volume to(required) |
| :type node: :class:`Node` |
| |
| :param volume: Volume to be attached (required) |
| :type volume: :class:`StorageVolume` |
| |
| :keyword persist_across_boots: Wether volume should be \ |
| attached to node across boots |
| :type persist_across_boots: `bool` |
| |
| :rtype: :class: `StorageVolume` |
| """ |
| if not isinstance(volume, StorageVolume): |
| raise LinodeExceptionV4("Invalid volume instance") |
| |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| if volume.extra["linode_id"] is not None: |
| raise LinodeExceptionV4("Volume is already attached to a node") |
| |
| if node.extra["location"] != volume.extra["location"]: |
| raise LinodeExceptionV4("Volume and node " "must be on the same region") |
| |
| attr = {"linode_id": int(node.id), "persist_across_boots": persist_across_boots} |
| |
| response = self.connection.request( |
| "/v4/volumes/%s/attach" % volume.id, data=json.dumps(attr), method="POST" |
| ).object |
| return self._to_volume(response) |
| |
| def detach_volume(self, volume): |
| """Detaches a volume from a node. |
| |
| :param volume: Volume to be detached (required) |
| :type volume: :class:`StorageVolume` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(volume, StorageVolume): |
| raise LinodeExceptionV4("Invalid volume instance") |
| |
| if volume.extra["linode_id"] is None: |
| raise LinodeExceptionV4("Volume is already detached") |
| |
| response = self.connection.request("/v4/volumes/%s/detach" % volume.id, method="POST") |
| return response.status == httplib.OK |
| |
| def destroy_volume(self, volume): |
| """Destroys the volume given. |
| |
| :param volume: Volume to be deleted (required) |
| :type volume: :class:`StorageVolume` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(volume, StorageVolume): |
| raise LinodeExceptionV4("Invalid volume instance") |
| |
| if volume.extra["linode_id"] is not None: |
| raise LinodeExceptionV4("Volume must be detached" " before it can be deleted.") |
| response = self.connection.request("/v4/volumes/%s" % volume.id, method="DELETE") |
| return response.status == httplib.OK |
| |
| def ex_resize_volume(self, volume, size): |
| """Resizes the volume given. |
| |
| :param volume: Volume to be resized |
| :type volume: :class:`StorageVolume` |
| |
| :param size: new volume size in gigabytes, must be\ |
| greater than current size |
| :type size: `int` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(volume, StorageVolume): |
| raise LinodeExceptionV4("Invalid volume instance") |
| |
| if volume.size >= size: |
| raise LinodeExceptionV4("Volumes can only be resized up") |
| attr = {"size": size} |
| |
| response = self.connection.request( |
| "/v4/volumes/%s/resize" % volume.id, data=json.dumps(attr), method="POST" |
| ) |
| return response.status == httplib.OK |
| |
| def ex_clone_volume(self, volume, name): |
| """Clones the volume given |
| |
| :param volume: Volume to be cloned |
| :type volume: :class:`StorageVolume` |
| |
| :param name: new cloned volume name |
| :type name: `str` |
| |
| :rtype: :class:`StorageVolume` |
| """ |
| |
| if not isinstance(volume, StorageVolume): |
| raise LinodeExceptionV4("Invalid volume instance") |
| |
| attr = {"label": name} |
| response = self.connection.request( |
| "/v4/volumes/%s/clone" % volume.id, data=json.dumps(attr), method="POST" |
| ).object |
| |
| return self._to_volume(response) |
| |
| def ex_get_volume(self, volume_id): |
| """ |
| Return a Volume object based on a volume ID. |
| |
| :param volume_id: Volume's id |
| :type volume_id: ``str`` |
| |
| :return: A StorageVolume object for the volume |
| :rtype: :class:`StorageVolume` |
| """ |
| response = self.connection.request("/v4/volumes/%s" % volume_id).object |
| return self._to_volume(response) |
| |
| def create_image(self, disk, name=None, description=None): |
| """Creates a private image from a LinodeDisk. |
| Images are limited to three per account. |
| |
| :param disk: LinodeDisk to create the image from (required) |
| :type disk: :class:`LinodeDisk` |
| |
| :keyword name: A name for the image.\ |
| Defaults to the name of the disk \ |
| it is being created from if not provided |
| :type name: `str` |
| |
| :keyword description: A description of the image |
| :type description: `str` |
| |
| :return: The newly created NodeImage |
| :rtype: :class:`NodeImage` |
| """ |
| |
| if not isinstance(disk, LinodeDisk): |
| raise LinodeExceptionV4("Invalid disk instance") |
| |
| attr = {"disk_id": int(disk.id), "label": name, "description": description} |
| |
| response = self.connection.request( |
| "/v4/images", data=json.dumps(attr), method="POST" |
| ).object |
| return self._to_image(response) |
| |
| def delete_image(self, image): |
| """Deletes a private image |
| |
| :param image: NodeImage to delete (required) |
| :type image: :class:`NodeImage` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(image, NodeImage): |
| raise LinodeExceptionV4("Invalid image instance") |
| |
| response = self.connection.request("/v4/images/%s" % image.id, method="DELETE") |
| return response.status == httplib.OK |
| |
| def ex_list_addresses(self): |
| """List IP addresses |
| |
| :return: LinodeIPAddress list |
| :rtype: `list` of :class:`LinodeIPAddress` |
| """ |
| data = self._paginated_request("/v4/networking/ips", "data") |
| |
| return [self._to_address(obj) for obj in data] |
| |
| def ex_list_node_addresses(self, node): |
| """List all IPv4 addresses attached to node |
| |
| :param node: Node to list IP addresses |
| :type node: :class:`Node` |
| |
| :return: LinodeIPAddress list |
| :rtype: `list` of :class:`LinodeIPAddress` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| response = self.connection.request("/v4/linode/instances/%s/ips" % node.id).object |
| return self._to_addresses(response) |
| |
| def ex_allocate_private_address(self, node, address_type="ipv4"): |
| """Allocates a private IPv4 address to node.Only ipv4 is currently supported |
| |
| :param node: Node to attach the IP address |
| :type node: :class:`Node` |
| |
| :keyword address_type: Type of IP address |
| :type address_type: `str` |
| |
| :return: The newly created LinodeIPAddress |
| :rtype: :class:`LinodeIPAddress` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| # Only ipv4 is currently supported |
| if address_type != "ipv4": |
| raise LinodeExceptionV4("Address type not supported") |
| # Only one private IP address can be allocated |
| if len(node.private_ips) >= 1: |
| raise LinodeExceptionV4("Nodes can have up to one private IP") |
| |
| attr = {"public": False, "type": address_type} |
| |
| response = self.connection.request( |
| "/v4/linode/instances/%s/ips" % node.id, |
| data=json.dumps(attr), |
| method="POST", |
| ).object |
| return self._to_address(response) |
| |
| def ex_share_address(self, node, addresses): |
| """Shares an IP with another node.This can be used to allow one Linode |
| to begin serving requests should another become unresponsive. |
| |
| :param node: Node to share the IP addresses with |
| :type node: :class:`Node` |
| |
| :keyword addresses: List of IP addresses to share |
| :type address_type: `list` of :class: `LinodeIPAddress` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| if not all(isinstance(address, LinodeIPAddress) for address in addresses): |
| raise LinodeExceptionV4("Invalid address instance") |
| |
| attr = { |
| "ips": [address.inet for address in addresses], |
| "linode_id": int(node.id), |
| } |
| response = self.connection.request( |
| "/v4/networking/ipv4/share", data=json.dumps(attr), method="POST" |
| ) |
| return response.status == httplib.OK |
| |
| def ex_resize_node(self, node, size, allow_auto_disk_resize=False): |
| """ |
| Resizes a node the API Key has read_write permission |
| to a different Type. |
| The following requirements must be met: |
| - The node must not have a pending migration |
| - The account cannot have an outstanding balance |
| - The node must not have more disk allocation than the new size allows |
| |
| :param node: the Linode to resize |
| :type node: :class:`Node` |
| |
| :param size: the size of the new node |
| :type size: :class:`NodeSize` |
| |
| :keyword allow_auto_disk_resize: Automatically resize disks \ |
| when resizing a node. |
| :type allow_auto_disk_resize: ``bool`` |
| |
| :rtype: ``bool`` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| if not isinstance(size, NodeSize): |
| raise LinodeExceptionV4("Invalid node size") |
| |
| attr = {"type": size.id, "allow_auto_disk_resize": allow_auto_disk_resize} |
| |
| response = self.connection.request( |
| "/v4/linode/instances/%s/resize" % node.id, |
| data=json.dumps(attr), |
| method="POST", |
| ) |
| |
| return response.status == httplib.OK |
| |
| def ex_rename_node(self, node, name): |
| """Renames a node |
| |
| :param node: the Linode to resize |
| :type node: :class:`Node` |
| |
| :param name: the node's new name |
| :type name: ``str`` |
| |
| :return: Changed Node |
| :rtype: :class:`Node` |
| """ |
| if not isinstance(node, Node): |
| raise LinodeExceptionV4("Invalid node instance") |
| |
| attr = {"label": name} |
| |
| response = self.connection.request( |
| "/v4/linode/instances/%s" % node.id, data=json.dumps(attr), method="PUT" |
| ).object |
| |
| return self._to_node(response) |
| |
| def _to_node(self, data): |
| extra = { |
| "tags": data["tags"], |
| "location": data["region"], |
| "ipv6": data["ipv6"], |
| "hypervisor": data["hypervisor"], |
| "specs": data["specs"], |
| "alerts": data["alerts"], |
| "backups": data["backups"], |
| "watchdog_enabled": data["watchdog_enabled"], |
| } |
| |
| public_ips = [ip for ip in data["ipv4"] if not is_private_subnet(ip)] |
| private_ips = [ip for ip in data["ipv4"] if is_private_subnet(ip)] |
| return Node( |
| id=data["id"], |
| name=data["label"], |
| state=self.LINODE_STATES[data["status"]], |
| public_ips=public_ips, |
| private_ips=private_ips, |
| driver=self, |
| size=data["type"], |
| image=data["image"], |
| created_at=self._to_datetime(data["created"]), |
| extra=extra, |
| ) |
| |
| def _to_datetime(self, strtime): |
| return datetime.strptime(strtime, "%Y-%m-%dT%H:%M:%S") |
| |
| def _to_size(self, data): |
| extra = { |
| "class": data["class"], |
| "monthly_price": data["price"]["monthly"], |
| "addons": data["addons"], |
| "successor": data["successor"], |
| "transfer": data["transfer"], |
| "vcpus": data["vcpus"], |
| "gpus": data["gpus"], |
| } |
| return NodeSize( |
| id=data["id"], |
| name=data["label"], |
| ram=data["memory"], |
| disk=data["disk"], |
| bandwidth=data["network_out"], |
| price=data["price"]["hourly"], |
| driver=self, |
| extra=extra, |
| ) |
| |
| def _to_image(self, data): |
| extra = { |
| "type": data["type"], |
| "description": data["description"], |
| "created": self._to_datetime(data["created"]), |
| "created_by": data["created_by"], |
| "is_public": data["is_public"], |
| "size": data["size"], |
| "eol": data["eol"], |
| "vendor": data["vendor"], |
| } |
| return NodeImage(id=data["id"], name=data["label"], driver=self, extra=extra) |
| |
| def _to_location(self, data): |
| extra = { |
| "status": data["status"], |
| "capabilities": data["capabilities"], |
| "resolvers": data["resolvers"], |
| } |
| return NodeLocation( |
| id=data["id"], |
| name=data["id"], |
| country=data["country"].upper(), |
| driver=self, |
| extra=extra, |
| ) |
| |
| def _to_volume(self, data): |
| extra = { |
| "created": self._to_datetime(data["created"]), |
| "tags": data["tags"], |
| "location": data["region"], |
| "linode_id": data["linode_id"], |
| "linode_label": data["linode_label"], |
| "state": self.LINODE_VOLUME_STATES[data["status"]], |
| "filesystem_path": data["filesystem_path"], |
| } |
| return StorageVolume( |
| id=str(data["id"]), |
| name=data["label"], |
| size=data["size"], |
| driver=self, |
| extra=extra, |
| ) |
| |
| def _to_disk(self, data): |
| return LinodeDisk( |
| id=data["id"], |
| state=self.LINODE_DISK_STATES[data["status"]], |
| name=data["label"], |
| filesystem=data["filesystem"], |
| size=data["size"], |
| driver=self, |
| ) |
| |
| def _to_address(self, data): |
| extra = { |
| "gateway": data["gateway"], |
| "subnet_mask": data["subnet_mask"], |
| "prefix": data["prefix"], |
| "rdns": data["rdns"], |
| "node_id": data["linode_id"], |
| "region": data["region"], |
| } |
| return LinodeIPAddress( |
| inet=data["address"], |
| public=data["public"], |
| version=data["type"], |
| driver=self, |
| extra=extra, |
| ) |
| |
| def _to_addresses(self, data): |
| addresses = data["ipv4"]["public"] + data["ipv4"]["private"] |
| return [self._to_address(address) for address in addresses] |
| |
| def _paginated_request(self, url, obj, params=None): |
| """ |
| Perform multiple calls in order to have a full list of elements when |
| the API responses are paginated. |
| |
| :param url: API endpoint |
| :type url: ``str`` |
| |
| :param obj: Result object key |
| :type obj: ``str`` |
| |
| :param params: Request parameters |
| :type params: ``dict`` |
| |
| :return: ``list`` of API response objects |
| :rtype: ``list`` |
| """ |
| objects = [] |
| params = params if params is not None else {} |
| |
| ret = self.connection.request(url, params=params).object |
| |
| data = list(ret.get(obj, [])) |
| current_page = int(ret.get("page", 1)) |
| num_of_pages = int(ret.get("pages", 1)) |
| objects.extend(data) |
| for page in range(current_page + 1, num_of_pages + 1): |
| # add param to request next page |
| params["page"] = page |
| ret = self.connection.request(url, params=params).object |
| data = list(ret.get(obj, [])) |
| objects.extend(data) |
| return objects |