You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by je...@apache.org on 2010/08/05 21:40:58 UTC
svn commit: r982748 - /incubator/libcloud/trunk/libcloud/drivers/linode.py
Author: jed
Date: Thu Aug 5 19:40:58 2010
New Revision: 982748
URL: http://svn.apache.org/viewvc?rev=982748&view=rev
Log:
Document Linode driver and remove stale TODOs
Modified:
incubator/libcloud/trunk/libcloud/drivers/linode.py
Modified: incubator/libcloud/trunk/libcloud/drivers/linode.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/drivers/linode.py?rev=982748&r1=982747&r2=982748&view=diff
==============================================================================
--- incubator/libcloud/trunk/libcloud/drivers/linode.py (original)
+++ incubator/libcloud/trunk/libcloud/drivers/linode.py Thu Aug 5 19:40:58 2010
@@ -12,17 +12,21 @@
# 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 <js...@linode.com>
-#
-# BETA TESTING THE LINODE API AND DRIVERS
-#
-# A beta account that incurs no financial charge may be arranged for. Please
-# contact Jed Smith <js...@linode.com> for your request.
-#
-"""
-Linode driver
-"""
+
+"""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.
+
+Maintainer: Jed Smith <je...@linode.com>"""
+
from libcloud.types import Provider, NodeState, InvalidCredsException
from libcloud.base import ConnectionKey, Response
from libcloud.base import NodeDriver, NodeSize, Node, NodeLocation
@@ -31,25 +35,8 @@ from libcloud.base import NodeImage
from copy import copy
import os
-# JSON is included in the standard library starting with Python 2.6. For 2.5
-# and 2.4, there's a simplejson egg at: http://pypi.python.org/pypi/simplejson
-try: import json
-except: import simplejson as json
-
-
-class LinodeException(Exception):
- """
- Exception class for Linode driver
- """
- def __str__(self):
- return "(%u) %s" % (self.args[0], self.args[1])
- def __repr__(self):
- return "<LinodeException code %u '%s'>" % (self.args[0], self.args[1])
-
-# For beta accounts, change this to "beta.linode.com".
+# Where requests go - in beta situations, this information may change.
LINODE_API = "api.linode.com"
-
-# For beta accounts, change this to "/api/".
LINODE_ROOT = "/"
# Map of TOTALRAM to PLANID, allows us to figure out what plan
@@ -65,12 +52,44 @@ LINODE_PLAN_IDS = {512:'1',
16384:'9',
20480:'10'}
+# JSON is included in the standard library starting with Python 2.6. For 2.5
+# and 2.4, there's a simplejson egg at: http://pypi.python.org/pypi/simplejson
+try: import json
+except: import simplejson as json
+
+
+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 __str__(self):
+ return "(%u) %s" % (self.args[0], self.args[1])
+ def __repr__(self):
+ return "<LinodeException code %u '%s'>" % (self.args[0], self.args[1])
+
class LinodeResponse(Response):
- # Wraps a Linode API HTTP response.
+ """Linode API response
+
+ Wraps the HTTP response returned by the Linode API, which should be JSON in
+ this structure:
+ {
+ "ERRORARRAY": [ ... ],
+ "DATA": [ ... ],
+ "ACTION": " ... "
+ }
+
+ 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."""
def __init__(self, response):
- # Given a response object, slurp the information from it.
+ """Instantiate a LinodeResponse from the HTTP response
+
+ @keyword response: The raw response returned by urllib
+ @return: parsed L{LinodeResponse}"""
self.body = response.read()
self.status = response.status
self.headers = dict(response.getheaders())
@@ -81,25 +100,22 @@ class LinodeResponse(Response):
# Move parse_body() to here; we can't be sure of failure until we've
# parsed the body into JSON.
self.action, self.object, self.errors = self.parse_body()
-
- if self.error == "Moved Temporarily":
- raise LinodeException(0xFA,
- "Redirected to error page by API. Bug?")
-
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. Will return None if the
- # JSON response chokes the parser. Returns a triple:
- # (action, data, errorarray)
+ """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: triple of action (C{str}), data, and errors (C{list})"""
try:
js = json.loads(self.body)
if ("DATA" not in js
or "ERRORARRAY" not in js
or "ACTION" not in js):
-
return (None, None, [self.invalid])
errs = [self._make_excp(e) for e in js["ERRORARRAY"]]
return (js["ACTION"], js["DATA"], errs)
@@ -108,7 +124,9 @@ class LinodeResponse(Response):
return (None, None, [self.invalid])
def parse_error(self):
- # Obtain the errors from the response. Will always return a list.
+ """Parse errors from the response
+
+ @return: C{list} of errors, possibly empty"""
try:
js = json.loads(self.body)
if "ERRORARRAY" not in js:
@@ -118,30 +136,38 @@ class LinodeResponse(Response):
return [self.invalid]
def success(self):
- # Does the response indicate success? If ERRORARRAY has more than one
- # entry, we'll say no.
+ """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: C{bool} indicating a successful request"""
return len(self.errors) == 0
def _make_excp(self, error):
- # Make an exception from an entry in ERRORARRAY.
+ """Convert an API error to a LinodeException instance
+
+ @keyword error: JSON object containing C{ERRORCODE} and C{ERRORMESSAGE}
+ @type error: dict"""
if "ERRORCODE" not in error or "ERRORMESSAGE" not in error:
return None
- if error["ERRORCODE"] == 4:
+ if error["ERRORCODE"] == 4:
return InvalidCredsException(error["ERRORMESSAGE"])
return LinodeException(error["ERRORCODE"], error["ERRORMESSAGE"])
class LinodeConnection(ConnectionKey):
- """
- Connection class for the LinodeConnection driver
-
- Wraps Linode HTTPS connection, and passes along the connection key.
- """
+ """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 = LINODE_API
responseCls = LinodeResponse
def add_default_params(self, params):
+ """Add parameters that are necessary for every request
+
+ This method adds C{api_key} and C{api_responseFormat} to the request."""
params["api_key"] = self.key
# Be explicit about this in case the default changes.
params["api_responseFormat"] = "json"
@@ -149,10 +175,24 @@ class LinodeConnection(ConnectionKey):
class LinodeNodeDriver(NodeDriver):
- """
- Linode node driver
+ """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
- The meat of Linode operations.
+ For more information on the Linode API, be sure to read the reference:
+
+ http://www.linode.com/api/
"""
type = Provider.LINODE
name = "Linode"
@@ -160,6 +200,10 @@ class LinodeNodeDriver(NodeDriver):
_linode_plan_ids = LINODE_PLAN_IDS
def __init__(self, key):
+ """Instantiate the driver with the given API key
+
+ @keyword key: the API key to use
+ @type key: C{str}"""
self.datacenter = None
NodeDriver.__init__(self, key)
@@ -170,61 +214,104 @@ class LinodeNodeDriver(NodeDriver):
-1: NodeState.PENDING, # Being Created
0: NodeState.PENDING, # Brand New
1: NodeState.RUNNING, # Running
- 2: NodeState.REBOOTING, # Powered Off (TODO: Extra state?)
- 3: NodeState.REBOOTING, # Shutting Down (?)
+ 2: NodeState.REBOOTING, # Powered Off
+ 3: NodeState.REBOOTING, # Shutting Down
4: NodeState.UNKNOWN # Reserved
}
def list_nodes(self):
- # List
- # Provide a list of all nodes that this API key has access to.
+ """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: C{list} of L{Node} objects that the API key can access"""
params = { "api_action": "linode.list" }
data = self.connection.request(LINODE_ROOT, params=params).object
return [self._to_node(n) for n in data]
def reboot_node(self, node):
- # Reboot
- # Execute a shutdown and boot job for the given 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.
+
+ @keyword node: the Linode to reboot
+ @type node: L{Node}"""
params = { "api_action": "linode.reboot", "LinodeID": node.id }
self.connection.request(LINODE_ROOT, params=params)
return True
def destroy_node(self, node):
- # Destroy
- # Terminates a Node. With prejudice.
+ """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.
+
+ @keyword node: the Linode to destroy
+ @type node: L{Node}"""
params = { "api_action": "linode.delete", "LinodeID": node.id,
"skipChecks": True }
self.connection.request(LINODE_ROOT, params=params)
return True
def create_node(self, **kwargs):
- """Create a new linode instance
+ """Create a new Linode, deploy a Linux distribution, and boot
- See L{NodeDriver.create_node} for more keyword args.
+ 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.
- @keyword ex_swap: Size of the swap partition in MB (128).
- @type ex_swap: C{number}
+ Note that there is a safety valve of 5 Linodes per hour, in order to
+ prevent a runaway script from ruining your day.
- @keyword ex_rsize: Size of the root partition (plan size - swap).
- @type ex_rsize: C{number}
+ @keyword name: the name to assign the Linode (mandatory)
+ @type name: C{str}
- @keyword ex_kernel: A kernel ID from avail.kernels (Latest 2.6).
- @type ex_kernel: C{number}
+ @keyword image: which distribution to deploy on the Linode (mandatory)
+ @type image: L{NodeImage}
- @keyword ex_payment: One of 1, 12, or 24; subscription length (1)
- @type ex_payment: C{number}
+ @keyword size: the plan size to create (mandatory)
+ @type size: L{NodeSize}
- @keyword ex_comment: Comments to store with the config
- @type ex_comment: C{str}
- """
- # Labels to override what's generated (default on right):
- # lconfig [%name] Instance
- # lrecovery [%name] Finnix Recovery Configuration
- # lroot [%name] %distro
- # lswap [%name] Swap Space
- #
- # Please note that for safety, only 5 Linodes can be created per hour.
+ @keyword auth: an SSH key or root password (mandatory)
+ @type auth: L{NodeAuthSSHKey} or L{NodeAuthPassword}
+
+ @keyword location: which datacenter to create the Linode in
+ @type location: L{NodeLocation}
+ @keyword ex_swap: size of the swap partition in MB (128)
+ @type ex_swap: C{int}
+
+ @keyword ex_rsize: size of the root partition in MB (plan size - swap).
+ @type ex_rsize: C{int}
+
+ @keyword ex_kernel: a kernel ID from avail.kernels (Latest 2.6 Stable).
+ @type ex_kernel: C{str}
+
+ @keyword ex_payment: one of 1, 12, or 24; subscription length (1)
+ @type ex_payment: C{int}
+
+ @keyword ex_comment: a small comment for the configuration (libcloud)
+ @type ex_comment: C{str}
+
+ @keyword lconfig: what to call the configuration (generated)
+ @type lconfig: C{str}
+
+ @keyword lroot: what to call the root image (generated)
+ @type lroot: C{str}
+
+ @keyword lswap: what to call the swap space (generated)
+ @type lswap: C{str}
+
+ @return: a L{Node} representing the newly-created Linode
+ """
name = kwargs["name"]
image = kwargs["image"]
size = kwargs["size"]
@@ -295,17 +382,16 @@ class LinodeNodeDriver(NodeDriver):
raise LinodeException(0xFB, "Invalid kernel -- avail.kernels")
# Comments
- comments = "Created by libcloud <http://www.libcloud.org>" if \
+ comments = "Created by Apache libcloud <http://www.libcloud.org>" if \
"ex_comment" not in kwargs else kwargs["ex_comment"]
# Labels
label = {
"lconfig": "[%s] Configuration Profile" % name,
- "lrecovery": "[%s] Finnix Recovery Configuration" % name,
"lroot": "[%s] %s Disk Image" % (name, image.name),
"lswap": "[%s] Swap Space" % name
}
- for what in ["lconfig", "lrecovery", "lroot", "lswap"]:
+ for what in ["lconfig", "lroot", "lswap"]:
if what in kwargs:
label[what] = kwargs[what]
@@ -366,8 +452,6 @@ class LinodeNodeDriver(NodeDriver):
data = self.connection.request(LINODE_ROOT, params=params).object
linode["config"] = data["ConfigID"]
- # TODO: Recovery image (Finnix)
-
# Step 5: linode.boot
params = {
"api_action": "linode.boot",
@@ -382,9 +466,16 @@ class LinodeNodeDriver(NodeDriver):
return self._to_node(data[0])
def list_sizes(self, location=None):
- # List Sizes
- # Retrieve all available Linode plans.
- # FIXME: Prices get mangled due to 'float'.
+ """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: NodeLocation
+
+ @return: a C{list} of L{NodeSize}s"""
params = { "api_action": "avail.linodeplans" }
data = self.connection.request(LINODE_ROOT, params=params).object
sizes = []
@@ -395,9 +486,12 @@ class LinodeNodeDriver(NodeDriver):
sizes.append(n)
return sizes
- def list_images(self, location=None):
- # List Images
- # Retrieve all available Linux distributions.
+ def list_images(self):
+ """List available Linux distributions
+
+ Retrieve all Linux distributions that can be deployed to a Linode.
+
+ @return: a C{list} of L{NodeImage}s"""
params = { "api_action": "avail.distributions" }
data = self.connection.request(LINODE_ROOT, params=params).object
distros = []
@@ -411,33 +505,38 @@ class LinodeNodeDriver(NodeDriver):
return distros
def list_locations(self):
+ """List available facilities for deployment
+
+ Retrieve all facilities that a Linode can be deployed in.
+
+ @return: a C{list} of L{NodeLocation}s"""
params = { "api_action": "avail.datacenters" }
data = self.connection.request(LINODE_ROOT, params=params).object
nl = []
for dc in data:
country = None
- #TODO: this is a hack!
- if dc["LOCATION"][-3:] == "USA":
- country = "US"
- elif dc["LOCATION"][-2:] == "UK":
- country = "GB"
- else:
- raise LinodeException(
- 0xFD,
- "Unable to convert data center location to country: '%s'"
- % dc["LOCATION"]
- )
+ if "USA" in dc["LOCATION"]: country = "US"
+ elif "UK" in dc["LOCATION"]: country = "GB"
+ else: country = "??"
nl.append(NodeLocation(dc["DATACENTERID"],
dc["LOCATION"],
country,
self))
return nl
- def linode_set_datacenter(self, did):
- # Set the datacenter for create requests.
+ 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 L{create_node} will use. If a C{location} keyword is not
+ passed to L{create_node}, this method must have already been used.
+
+ @keyword dc: the datacenter to create Linodes in unless specified
+ @type dc: L{NodeLocation}"""
+ did = dc.id
params = { "api_action": "avail.datacenters" }
data = self.connection.request(LINODE_ROOT, params=params).object
- for dc in data:
+ for datacenter in data:
if did == dc["DATACENTERID"]:
self.datacenter = did
return
@@ -447,10 +546,14 @@ class LinodeNodeDriver(NodeDriver):
raise LinodeException(0xFD, "Invalid datacenter (use one of %s)" % dcs)
def _to_node(self, obj):
- # Convert a returned Linode instance into a Node instance.
- lid = obj["LINODEID"]
+ """Convert a returned JSON Linode into a Node instance
+
+ @keyword obj: a JSON dictionary representing the Linode
+ @type obj: C{dict}
+ @return: L{Node}"""
+ lid = str(obj["LINODEID"])
- # Get the IP addresses for a Linode
+ # Get the IP addresses for the Linode
params = { "api_action": "linode.ip.list", "LinodeID": lid }
req = self.connection.request(LINODE_ROOT, params=params)
if not req.success() or len(req.object) == 0: