Libcloud now a tlp
git-svn-id: https://svn.apache.org/repos/asf/libcloud/branches/0.2.x@1127028 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..20476a2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+*.py[co]
+*.sw[po]
+test/secrets.py
+*~
+_trial_temp
+build
diff --git a/.ratignore b/.ratignore
new file mode 100644
index 0000000..da482b2
--- /dev/null
+++ b/.ratignore
@@ -0,0 +1,4 @@
+MANIFEST.in
+.gitignore
+apidocs/
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..8935287
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include LICENSE
+include NOTICE
+include example.py
+include CONTRIBUTORS
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..a5a92a1
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,9 @@
+libcloud
+
+Copyright (c) 2009 The Apache Software Foundation.
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes software developed by
+Cloudkick (http://www.cloudkick.com/).
diff --git a/README b/README
new file mode 100644
index 0000000..13ee4d2
--- /dev/null
+++ b/README
@@ -0,0 +1,7 @@
+Apache libcloud - a unified interface into the cloud
+
+The goal of this project is to create a basic yet functional standard library
+into various cloud providers.
+
+Apache libcloud is an incubator project at the Apache Software Foundation, see
+<http://incubator.apache.org/libcloud> for more information.
\ No newline at end of file
diff --git a/example.py b/example.py
new file mode 100644
index 0000000..d501a59
--- /dev/null
+++ b/example.py
@@ -0,0 +1,36 @@
+# 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.
+# libcloud.org 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.
+from libcloud.types import Provider
+from libcloud.providers import get_driver
+
+EC2 = get_driver(Provider.EC2_US_EAST)
+Slicehost = get_driver(Provider.SLICEHOST)
+Rackspace = get_driver(Provider.RACKSPACE)
+
+drivers = [ EC2('access key id', 'secret key'),
+ Slicehost('api key'),
+ Rackspace('username', 'api key') ]
+
+nodes = [ driver.list_nodes() for driver in drivers ]
+
+print nodes
+# [ <Node: provider=Amazon, status=RUNNING, name=bob, ip=1.2.3.4.5>,
+# <Node: provider=Slicehost, status=REBOOT, name=korine, ip=6.7.8.9.10>, ... ]
+
+# grab the node named "test"
+node = filter(lambda x: x.name == 'test', nodes)[0]
+
+# reboot "test"
+node.reboot()
diff --git a/libcloud/__init__.py b/libcloud/__init__.py
new file mode 100644
index 0000000..72d8f54
--- /dev/null
+++ b/libcloud/__init__.py
@@ -0,0 +1,48 @@
+# 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.
+# libcloud.org 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 provides a unified interface to the cloud computing resources.
+
+@var __version__: Current version of libcloud
+"""
+
+__all__ = ["__version__", "enable_debug"]
+
+__version__ = "0.2.1-dev"
+
+
+def enable_debug(fo):
+ """
+ Enable library wide debugging to a file-like object.
+
+ @param fo: Where to append debugging information
+ @type fo: File like object, only write operations are used.
+ """
+ import httplib
+ from libcloud.base import ConnectionKey,LoggingHTTPSConnection
+ LoggingHTTPSConnection.log = fo
+ ConnectionKey.conn_classes = (httplib.HTTPConnection, LoggingHTTPSConnection)
+
+def _init_once():
+ import os
+ d = os.getenv("LIBCLOUD_DEBUG")
+ if d:
+ if d.isdigit():
+ d = "/tmp/libcloud_debug.log"
+ fo = open(d, "a")
+ enable_debug(fo)
+
+_init_once()
diff --git a/libcloud/base.py b/libcloud/base.py
new file mode 100644
index 0000000..473617c
--- /dev/null
+++ b/libcloud/base.py
@@ -0,0 +1,546 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Provides base classes for working with drivers
+"""
+import httplib, urllib
+import libcloud
+from zope import interface
+from libcloud.interface import IConnectionUserAndKey, IResponse
+from libcloud.interface import IConnectionKey, IConnectionKeyFactory
+from libcloud.interface import IConnectionUserAndKeyFactory, IResponseFactory
+from libcloud.interface import INodeDriverFactory, INodeDriver
+from libcloud.interface import INodeFactory, INode
+from libcloud.interface import INodeSizeFactory, INodeSize
+from libcloud.interface import INodeImageFactory, INodeImage
+import hashlib
+import StringIO
+from pipes import quote as pquote
+
+class Node(object):
+ """
+ A Base Node class to derive from.
+ """
+
+ interface.implements(INode)
+ interface.classProvides(INodeFactory)
+
+ def __init__(self, id, name, state, public_ip, private_ip,
+ driver, extra=None):
+ self.id = id
+ self.name = name
+ self.state = state
+ self.public_ip = public_ip
+ self.private_ip = private_ip
+ self.driver = driver
+ self.uuid = self.get_uuid()
+ if not extra:
+ self.extra = {}
+ else:
+ self.extra = extra
+
+ def get_uuid(self):
+ return hashlib.sha1("%s:%d" % (self.id,self.driver.type)).hexdigest()
+
+ def reboot(self):
+ return self.driver.reboot_node(self)
+
+ def destroy(self):
+ return self.driver.destroy_node(self)
+
+ def __repr__(self):
+ return (('<Node: uuid=%s, name=%s, state=%s, public_ip=%s, '
+ 'provider=%s ...>')
+ % (self.uuid, self.name, self.state, self.public_ip,
+ self.driver.name))
+
+
+class NodeSize(object):
+ """
+ A Base NodeSize class to derive from.
+ """
+
+ interface.implements(INodeSize)
+ interface.classProvides(INodeSizeFactory)
+
+ def __init__(self, id, name, ram, disk, bandwidth, price, driver):
+ self.id = id
+ self.name = name
+ self.ram = ram
+ self.disk = disk
+ self.bandwidth = bandwidth
+ self.price = price
+ self.driver = driver
+ def __repr__(self):
+ return (('<NodeSize: id=%s, name=%s, ram=%s disk=%s bandwidth=%s '
+ 'price=%s driver=%s ...>')
+ % (self.id, self.name, self.ram, self.disk, self.bandwidth,
+ self.price, self.driver.name))
+
+
+class NodeImage(object):
+ """
+ A Base NodeImage class to derive from.
+ """
+
+ interface.implements(INodeImage)
+ interface.classProvides(INodeImageFactory)
+
+ def __init__(self, id, name, driver, extra=None):
+ self.id = id
+ self.name = name
+ self.driver = driver
+ if not extra:
+ self.extra = {}
+ else:
+ self.extra = extra
+ def __repr__(self):
+ return (('<NodeImage: id=%s, name=%s, driver=%s ...>')
+ % (self.id, self.name, self.driver.name))
+
+class NodeLocation(object):
+ """
+ A base NodeLocation class to derive from.
+ """
+ interface.implements(INodeImage)
+ interface.classProvides(INodeImageFactory)
+ def __init__(self, id, name, country, driver):
+ self.id = id
+ self.name = name
+ self.country = country
+ self.driver = driver
+ def __repr__(self):
+ return (('<NodeLocation: id=%s, name=%s, country=%s, driver=%s>')
+ % (self.id, self.name, self.country, self.driver.name))
+
+class NodeAuthSSHKey(object):
+ def __init__(self, pubkey):
+ self.pubkey = pubkey
+ def __repr__(self):
+ return '<NodeAuthSSHKey>'
+
+class NodeAuthPassword(object):
+ def __init__(self, password):
+ self.password = password
+ def __repr__(self):
+ return '<NodeAuthPassword>'
+
+class Response(object):
+ """
+ A Base Response class to derive from.
+ """
+ interface.implements(IResponse)
+ interface.classProvides(IResponseFactory)
+
+ NODE_STATE_MAP = {}
+
+ object = None
+ body = None
+ status = httplib.OK
+ headers = {}
+ error = None
+ connection = None
+
+ def __init__(self, response):
+ self.body = response.read()
+ self.status = response.status
+ self.headers = dict(response.getheaders())
+ self.error = response.reason
+
+ if not self.success():
+ raise Exception(self.parse_error())
+
+ self.object = self.parse_body()
+
+ def parse_body(self):
+ """
+ Parse response body.
+
+ Override in a provider's subclass.
+
+ @return: Parsed body.
+ """
+ return self.body
+
+ def parse_error(self):
+ """
+ Parse the error messages.
+
+ Override in a provider's subclass.
+
+ @return: Parsed error.
+ """
+ return self.body
+
+ def success(self):
+ """
+ Determine if our request was successful.
+
+ The meaning of this can be arbitrary; did we receive OK status? Did
+ the node get created? Were we authenticated?
+
+ @return: C{True} or C{False}
+ """
+ return self.status == httplib.OK or self.status == httplib.CREATED
+
+#TODO: Move this to a better location/package
+class LoggingHTTPSConnection(httplib.HTTPSConnection):
+ """
+ Debug class to log all HTTP(s) requests as they could be made
+ with the C{curl} command.
+
+ @cvar log: file-like object that logs entries are written to.
+ """
+ log = None
+
+ def _log_response(self, r):
+ rv = "# -------- begin %d:%d response ----------\n" % (id(self), id(r))
+ ht = ""
+ v = r.version
+ if r.version == 10:
+ v = "HTTP/1.0"
+ if r.version == 11:
+ v = "HTTP/1.1"
+ ht += "%s %s %s\r\n" % (v, r.status, r.reason)
+ body = r.read()
+ for h in r.getheaders():
+ ht += "%s: %s\r\n" % (h[0].title(), h[1])
+ ht += "\r\n"
+ # this is evil. laugh with me. ha arharhrhahahaha
+ class fakesock:
+ def __init__(self, s):
+ self.s = s
+ def makefile(self, mode, foo):
+ return StringIO.StringIO(self.s)
+ rr = r
+ if r.chunked:
+ ht += "%x\r\n" % (len(body))
+ ht += body
+ ht += "\r\n0\r\n"
+ else:
+ ht += body
+ rr = httplib.HTTPResponse(fakesock(ht),
+ method=r._method,
+ debuglevel=r.debuglevel)
+ rr.begin()
+ rv += ht
+ rv += ("\n# -------- end %d:%d response ----------\n"
+ % (id(self), id(r)))
+ return (rr, rv)
+
+ def getresponse(self):
+ r = httplib.HTTPSConnection.getresponse(self)
+ if self.log is not None:
+ r, rv = self._log_response(r)
+ self.log.write(rv + "\n")
+ self.log.flush()
+ return r
+
+ def _log_curl(self, method, url, body, headers):
+ cmd = ["curl", "-i"]
+
+ cmd.extend(["-X", pquote(method)])
+
+ for h in headers:
+ cmd.extend(["-H", pquote("%s: %s" % (h, headers[h]))])
+
+ # TODO: in python 2.6, body can be a file-like object.
+ if body is not None and len(body) > 0:
+ cmd.extend(["--data-binary", pquote(body)])
+
+ cmd.extend([pquote("https://%s:%d%s" % (self.host, self.port, url))])
+ return " ".join(cmd)
+
+ def request(self, method, url, body=None, headers=None):
+ if self.log is not None:
+ pre = "# -------- begin %d request ----------\n" % id(self)
+ self.log.write(pre +
+ self._log_curl(method, url, body, headers) + "\n")
+ self.log.flush()
+ return httplib.HTTPSConnection.request(self, method, url,
+ body, headers)
+
+class ConnectionKey(object):
+ """
+ A Base Connection class to derive from.
+ """
+ interface.implementsOnly(IConnectionKey)
+ interface.classProvides(IConnectionKeyFactory)
+
+ #conn_classes = (httplib.HTTPConnection, LoggingHTTPSConnection)
+ conn_classes = (httplib.HTTPConnection, httplib.HTTPSConnection)
+
+ responseCls = Response
+ connection = None
+ host = '127.0.0.1'
+ port = (80, 443)
+ secure = 1
+ driver = None
+
+ def __init__(self, key, secure=True):
+ """
+ Initialize `user_id` and `key`; set `secure` to an C{int} based on
+ passed value.
+ """
+ self.key = key
+ self.secure = secure and 1 or 0
+ self.ua = []
+
+ def connect(self, host=None, port=None):
+ """
+ Establish a connection with the API server.
+
+ @type host: C{str}
+ @param host: Optional host to override our default
+
+ @type port: C{int}
+ @param port: Optional port to override our default
+
+ @returns: A connection
+ """
+ host = host or self.host
+ port = port or self.port[self.secure]
+
+ connection = self.conn_classes[self.secure](host, port)
+ self.connection = connection
+
+ def _user_agent(self):
+ return 'libcloud/%s (%s)%s' % (
+ libcloud.__version__,
+ self.driver.name,
+ "".join([" (%s)" % x for x in self.ua]))
+
+ def user_agent_append(self, s):
+ self.ua.append(s)
+
+ def request(self,
+ action,
+ params=None,
+ data='',
+ headers=None,
+ method='GET'):
+ """
+ Request a given `action`.
+
+ Basically a wrapper around the connection
+ object's `request` that does some helpful pre-processing.
+
+ @type action: C{str}
+ @param action: A path
+
+ @type params: C{dict}
+ @param params: Optional mapping of additional parameters to send. If
+ None, leave as an empty C{dict}.
+
+ @type data: C{unicode}
+ @param data: A body of data to send with the request.
+
+ @type headers: C{dict}
+ @param headers: Extra headers to add to the request
+ None, leave as an empty C{dict}.
+
+ @type method: C{str}
+ @param method: An HTTP method such as "GET" or "POST".
+
+ @return: An instance of type I{responseCls}
+ """
+ if params is None:
+ params = {}
+ if headers is None:
+ headers = {}
+ # Extend default parameters
+ params = self.add_default_params(params)
+ # Extend default headers
+ headers = self.add_default_headers(headers)
+ # We always send a content length and user-agent header
+ headers.update({'Content-Length': len(data)})
+ headers.update({'User-Agent': self._user_agent()})
+ headers.update({'Host': self.host})
+ # Encode data if necessary
+ if data != '':
+ data = self.encode_data(data)
+ url = '?'.join((action, urllib.urlencode(params)))
+
+ # Removed terrible hack...this a less-bad hack that doesn't execute a
+ # request twice, but it's still a hack.
+ self.connect()
+ self.connection.request(method=method, url=url, body=data,
+ headers=headers)
+ response = self.responseCls(self.connection.getresponse())
+ response.connection = self
+ return response
+
+ def add_default_params(self, params):
+ """
+ Adds default parameters (such as API key, version, etc.)
+ to the passed `params`
+
+ Should return a dictionary.
+ """
+ return params
+
+ def add_default_headers(self, headers):
+ """
+ Adds default headers (such as Authorization, X-Foo-Bar)
+ to the passed `headers`
+
+ Should return a dictionary.
+ """
+ return headers
+
+ def encode_data(self, data):
+ """
+ Encode body data.
+
+ Override in a provider's subclass.
+ """
+ return data
+
+
+class ConnectionUserAndKey(ConnectionKey):
+ """
+ Base connection which accepts a user_id and key
+ """
+ interface.implementsOnly(IConnectionUserAndKey)
+ interface.classProvides(IConnectionUserAndKeyFactory)
+
+ user_id = None
+
+ def __init__(self, user_id, key, secure=True):
+ super(ConnectionUserAndKey, self).__init__(key, secure)
+ self.user_id = user_id
+
+
+class NodeDriver(object):
+ """
+ A base NodeDriver class to derive from
+ """
+ interface.implements(INodeDriver)
+ interface.classProvides(INodeDriverFactory)
+
+ connectionCls = ConnectionKey
+ name = None
+ type = None
+ features = {"create_node": []}
+ """
+ List of available features for a driver.
+ - L{create_node}
+ - ssh_key: Supports L{NodeAuthSSHKey} as an authentication method
+ for nodes.
+ - password: Supports L{NodeAuthPassword} as an authentication
+ method for nodes.
+ """
+ NODE_STATE_MAP = {}
+
+ def __init__(self, key, secret=None, secure=True):
+ """
+ @keyword key: API key or username to used
+ @type key: str
+
+ @keyword secret: Secret password to be used
+ @type secret: str
+
+ @keyword secure: Weither to use HTTPS or HTTP. Note: Some providers
+ only support HTTPS, and it is on by default.
+ @type secure: bool
+ """
+ self.key = key
+ self.secret = secret
+ self.secure = secure
+ if self.secret:
+ self.connection = self.connectionCls(key, secret, secure)
+ else:
+ self.connection = self.connectionCls(key, secure)
+
+ self.connection.driver = self
+ self.connection.connect()
+
+ def create_node(self, **kwargs):
+ """Create a new node instance.
+
+ @keyword name: String with a name for this new node (required)
+ @type name: str
+
+ @keyword size: The size of resources allocated to this node.
+ (required)
+ @type size: L{NodeSize}
+
+ @keyword image: OS Image to boot on node. (required)
+ @type image: L{NodeImage}
+
+ @keyword location: Which data center to create a node in. If empty,
+ undefined behavoir will be selected. (optional)
+ @type location: L{NodeLocation}
+
+ @keyword auth: Initial authentication information for the node
+ (optional)
+ @type auth: L{NodeAuthSSHKey} or L{NodeAuthPassword}
+
+ @return: The newly created L{Node}.
+ """
+ raise NotImplementedError, \
+ 'create_node not implemented for this driver'
+
+ def destroy_node(self, node):
+ """Destroy a node.
+
+ Depending upon the provider, this may destroy all data associated with
+ the node, including backups.
+
+ @return: C{bool} True if the destroy was successful, otherwise False
+ """
+ raise NotImplementedError, \
+ 'destroy_node not implemented for this driver'
+
+ def reboot_node(self, node):
+ """
+ Reboot a node.
+ @return: C{bool} True if the reboot was successful, otherwise False
+ """
+ raise NotImplementedError, \
+ 'reboot_node not implemented for this driver'
+
+ def list_nodes(self):
+ """
+ List all nodes
+ @return: C{list} of L{Node} objects
+ """
+ raise NotImplementedError, \
+ 'list_nodes not implemented for this driver'
+
+ def list_images(self, location=None):
+ """
+ List images on a provider
+ @return: C{list} of L{NodeImage} objects
+ """
+ raise NotImplementedError, \
+ 'list_images not implemented for this driver'
+
+ def list_sizes(self, location=None):
+ """
+ List sizes on a provider
+ @return: C{list} of L{NodeSize} objects
+ """
+ raise NotImplementedError, \
+ 'list_sizes not implemented for this driver'
+
+ def list_locations(self):
+ """
+ List data centers for a provider
+ @return: C{list} of L{NodeLocation} objects
+ """
+ raise NotImplementedError, \
+ 'list_locations not implemented for this driver'
diff --git a/libcloud/drivers/__init__.py b/libcloud/drivers/__init__.py
new file mode 100644
index 0000000..8f7f612
--- /dev/null
+++ b/libcloud/drivers/__init__.py
@@ -0,0 +1,18 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Drivers for working with different providers
+"""
diff --git a/libcloud/drivers/dummy.py b/libcloud/drivers/dummy.py
new file mode 100644
index 0000000..517b6c8
--- /dev/null
+++ b/libcloud/drivers/dummy.py
@@ -0,0 +1,140 @@
+# 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.
+# libcloud.org 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.
+"""
+Dummy Driver
+
+@note: This driver is out of date
+"""
+from libcloud.interface import INodeDriver
+from libcloud.base import ConnectionKey, NodeDriver, NodeSize, NodeLocation
+from libcloud.base import NodeImage, Node
+from libcloud.types import Provider,NodeState
+from zope.interface import implements
+
+import uuid
+
+class DummyConnection(ConnectionKey):
+
+ def connect(self, host=None, port=None):
+ pass
+
+class DummyNodeDriver(NodeDriver):
+
+ name = "Dummy Node Provider"
+ type = Provider.DUMMY
+
+ implements(INodeDriver)
+
+ def __init__(self, creds):
+ self.creds = creds
+ self.nl = [
+ Node(id=1,
+ name='dummy-1',
+ state=NodeState.RUNNING,
+ public_ip=['127.0.0.1'],
+ private_ip=[],
+ driver=self,
+ extra={'foo': 'bar'}),
+ Node(id=2,
+ name='dummy-2',
+ state=NodeState.RUNNING,
+ public_ip=['127.0.0.1'],
+ private_ip=[],
+ driver=self,
+ extra={'foo': 'bar'}),
+ ]
+ self.connection = DummyConnection(self.creds)
+
+ def get_uuid(self, unique_field=None):
+ return str(uuid.uuid4())
+
+ def list_nodes(self):
+ return self.nl
+
+ def reboot_node(self, node):
+ node.state = NodeState.REBOOTING
+ return True
+
+ def destroy_node(self, node):
+ node.state = NodeState.TERMINATED
+ self.nl.remove(node)
+ return True
+
+ def list_images(self, location=None):
+ return [
+ NodeImage(id=1, name="Ubuntu 9.10", driver=self),
+ NodeImage(id=2, name="Ubuntu 9.04", driver=self),
+ NodeImage(id=3, name="Slackware 4", driver=self),
+ ]
+
+ def list_sizes(self, location=None):
+ return [
+ NodeSize(id=1,
+ name="Small",
+ ram=128,
+ disk=4,
+ bandwidth=500,
+ price=4,
+ driver=self),
+ NodeSize(id=2,
+ name="Medium",
+ ram=512,
+ disk=16,
+ bandwidth=1500,
+ price=8,
+ driver=self),
+ NodeSize(id=3,
+ name="Big",
+ ram=4096,
+ disk=32,
+ bandwidth=2500,
+ price=32,
+ driver=self),
+ NodeSize(id=4,
+ name="XXL Big",
+ ram=4096*2,
+ disk=32*4,
+ bandwidth=2500*3,
+ price=32*2,
+ driver=self),
+ ]
+
+ def list_locations(self):
+ return [
+ NodeLocation(id=1,
+ name="Paul's Room",
+ country='US',
+ driver=self),
+ NodeLocation(id=1,
+ name="London Loft",
+ country='GB',
+ driver=self),
+ NodeLocation(id=1,
+ name="Island Datacenter",
+ country='FJ',
+ driver=self),
+ ]
+
+ def create_node(self, **kwargs):
+ l = len(self.nl) + 1
+ n = Node(id=l,
+ name='dummy-%d' % l,
+ state=NodeState.RUNNING,
+ public_ip=['127.0.0.%d' % l],
+ private_ip=[],
+ driver=self,
+ extra={'foo': 'bar'})
+ self.nl.append(n)
+ return n
diff --git a/libcloud/drivers/ec2.py b/libcloud/drivers/ec2.py
new file mode 100644
index 0000000..228cac9
--- /dev/null
+++ b/libcloud/drivers/ec2.py
@@ -0,0 +1,434 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Amazon EC2 driver
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, Response, ConnectionUserAndKey
+from libcloud.base import NodeDriver, NodeSize, NodeImage, NodeLocation
+import base64
+import hmac
+from hashlib import sha256
+import time
+import urllib
+from xml.etree import ElementTree as ET
+
+EC2_US_EAST_HOST = 'ec2.us-east-1.amazonaws.com'
+EC2_US_WEST_HOST = 'ec2.us-west-1.amazonaws.com'
+EC2_EU_WEST_HOST = 'ec2.eu-west-1.amazonaws.com'
+
+API_VERSION = '2009-04-04'
+NAMESPACE = "http://ec2.amazonaws.com/doc/%s/" % (API_VERSION)
+
+"""
+Sizes must be hardcoded, because Amazon doesn't provide an API to fetch them.
+From http://aws.amazon.com/ec2/instance-types/
+"""
+EC2_INSTANCE_TYPES = {
+ 'm1.small': {
+ 'id': 'm1.small',
+ 'name': 'Small Instance',
+ 'ram': 1740,
+ 'disk': 160,
+ 'bandwidth': None
+ },
+ 'm1.large': {
+ 'id': 'm1.large',
+ 'name': 'Large Instance',
+ 'ram': 7680,
+ 'disk': 850,
+ 'bandwidth': None
+ },
+ 'm1.xlarge': {
+ 'id': 'm1.xlarge',
+ 'name': 'Extra Large Instance',
+ 'ram': 15360,
+ 'disk': 1690,
+ 'bandwidth': None
+ },
+ 'c1.medium': {
+ 'id': 'c1.medium',
+ 'name': 'High-CPU Medium Instance',
+ 'ram': 1740,
+ 'disk': 350,
+ 'bandwidth': None
+ },
+ 'c1.xlarge': {
+ 'id': 'c1.xlarge',
+ 'name': 'High-CPU Extra Large Instance',
+ 'ram': 7680,
+ 'disk': 1690,
+ 'bandwidth': None
+ },
+ 'm2.2xlarge': {
+ 'id': 'm2.2xlarge',
+ 'name': 'High-Memory Double Extra Large Instance',
+ 'ram': 35021,
+ 'disk': 850,
+ 'bandwidth': None
+ },
+ 'm2.4xlarge': {
+ 'id': 'm2.4xlarge',
+ 'name': 'High-Memory Quadruple Extra Large Instance',
+ 'ram': 70042,
+ 'disk': 1690,
+ 'bandwidth': None
+ },
+}
+
+EC2_US_EAST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+EC2_US_WEST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+EC2_EU_WEST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+
+EC2_US_EAST_INSTANCE_TYPES['m1.small']['price'] = '.085'
+EC2_US_EAST_INSTANCE_TYPES['m1.large']['price'] = '.34'
+EC2_US_EAST_INSTANCE_TYPES['m1.xlarge']['price'] = '.68'
+EC2_US_EAST_INSTANCE_TYPES['c1.medium']['price'] = '.17'
+EC2_US_EAST_INSTANCE_TYPES['c1.xlarge']['price'] = '.68'
+EC2_US_EAST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.2'
+EC2_US_EAST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.4'
+
+EC2_US_WEST_INSTANCE_TYPES['m1.small']['price'] = '.095'
+EC2_US_WEST_INSTANCE_TYPES['m1.large']['price'] = '.38'
+EC2_US_WEST_INSTANCE_TYPES['m1.xlarge']['price'] = '.76'
+EC2_US_WEST_INSTANCE_TYPES['c1.medium']['price'] = '.19'
+EC2_US_WEST_INSTANCE_TYPES['c1.xlarge']['price'] = '.76'
+EC2_US_WEST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.34'
+EC2_US_WEST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.68'
+
+EC2_EU_WEST_INSTANCE_TYPES['m1.small']['price'] = '.095'
+EC2_EU_WEST_INSTANCE_TYPES['m1.large']['price'] = '.38'
+EC2_EU_WEST_INSTANCE_TYPES['m1.xlarge']['price'] = '.76'
+EC2_EU_WEST_INSTANCE_TYPES['c1.medium']['price'] = '.19'
+EC2_EU_WEST_INSTANCE_TYPES['c1.xlarge']['price'] = '.76'
+EC2_EU_WEST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.34'
+EC2_EU_WEST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.68'
+
+class EC2Response(Response):
+
+ def parse_body(self):
+ if not self.body:
+ return None
+ return ET.XML(self.body)
+
+ def parse_error(self):
+ err_list = []
+ for err in ET.XML(self.body).findall('Errors/Error'):
+ code, message = err.getchildren()
+ err_list.append("%s: %s" % (code.text, message.text))
+ if code.text == "InvalidClientTokenId":
+ raise InvalidCredsException(err_list[-1])
+ if code.text == "SignatureDoesNotMatch":
+ raise InvalidCredsException(err_list[-1])
+ return "\n".join(err_list)
+
+class EC2Connection(ConnectionUserAndKey):
+
+ host = EC2_US_EAST_HOST
+ responseCls = EC2Response
+
+ def add_default_params(self, params):
+ params['SignatureVersion'] = '2'
+ params['SignatureMethod'] = 'HmacSHA256'
+ params['AWSAccessKeyId'] = self.user_id
+ params['Version'] = API_VERSION
+ params['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',
+ time.gmtime())
+ params['Signature'] = self._get_aws_auth_param(params, self.key)
+ return params
+
+ def _get_aws_auth_param(self, params, secret_key, path='/'):
+ """
+ Creates the signature required for AWS, per
+ http://bit.ly/aR7GaQ [docs.amazonwebservices.com]:
+
+ StringToSign = HTTPVerb + "\n" +
+ ValueOfHostHeaderInLowercase + "\n" +
+ HTTPRequestURI + "\n" +
+ CanonicalizedQueryString <from the preceding step>
+ """
+ keys = params.keys()
+ keys.sort()
+ pairs = []
+ for key in keys:
+ pairs.append(urllib.quote(key, safe='') + '=' +
+ urllib.quote(params[key], safe='-_~'))
+
+ qs = '&'.join(pairs)
+ string_to_sign = '\n'.join(('GET', self.host, path, qs))
+
+ b64_hmac = base64.b64encode(
+ hmac.new(secret_key, string_to_sign, digestmod=sha256).digest()
+ )
+ return b64_hmac
+
+class EC2NodeDriver(NodeDriver):
+
+ connectionCls = EC2Connection
+ type = Provider.EC2
+ name = 'Amazon EC2 (us-east-1)'
+
+ _instance_types = EC2_US_EAST_INSTANCE_TYPES
+
+ NODE_STATE_MAP = {
+ 'pending': NodeState.PENDING,
+ 'running': NodeState.RUNNING,
+ 'shutting-down': NodeState.TERMINATED,
+ 'terminated': NodeState.TERMINATED
+ }
+
+ def _findtext(self, element, xpath):
+ return element.findtext(self._fixxpath(xpath))
+
+ def _fixxpath(self, xpath):
+ # ElementTree wants namespaces in its xpaths, so here we add them.
+ return "/".join(["{%s}%s" % (NAMESPACE, e) for e in xpath.split("/")])
+
+ def _findattr(self, element, xpath):
+ return element.findtext(self._fixxpath(xpath))
+
+ def _findall(self, element, xpath):
+ return element.findall(self._fixxpath(xpath))
+
+ def _pathlist(self, key, arr):
+ """
+ Converts a key and an array of values into AWS query param format.
+ """
+ params = {}
+ i = 0
+ for value in arr:
+ i += 1
+ params["%s.%s" % (key, i)] = value
+ return params
+
+ def _get_boolean(self, element):
+ tag = "{%s}%s" % (NAMESPACE, 'return')
+ return element.findtext(tag) == 'true'
+
+ def _get_terminate_boolean(self, element):
+ status = element.findtext(".//{%s}%s" % (NAMESPACE, 'name'))
+ return any([ term_status == status
+ for term_status
+ in ('shutting-down', 'terminated') ])
+
+ def _to_nodes(self, object, xpath):
+ return [ self._to_node(el)
+ for el in object.findall(self._fixxpath(xpath)) ]
+
+ def _to_node(self, element):
+ try:
+ state = self.NODE_STATE_MAP[
+ self._findattr(element, "instanceState/name")
+ ]
+ except KeyError:
+ state = NodeState.UNKNOWN
+
+ n = Node(
+ id=self._findtext(element, 'instanceId'),
+ name=self._findtext(element, 'instanceId'),
+ state=state,
+ public_ip=[self._findtext(element, 'dnsName')],
+ private_ip=[self._findtext(element, 'privateDnsName')],
+ driver=self.connection.driver,
+ extra={
+ 'dns_name': self._findattr(element, "dnsName"),
+ 'instanceId': self._findattr(element, "instanceId"),
+ 'imageId': self._findattr(element, "imageId"),
+ 'private_dns': self._findattr(element, "privateDnsName"),
+ 'status': self._findattr(element, "instanceState/name"),
+ 'keyname': self._findattr(element, "keyName"),
+ 'launchindex': self._findattr(element, "amiLaunchIndex"),
+ 'productcode':
+ [p.text for p in self._findall(
+ element, "productCodesSet/item/productCode"
+ )],
+ 'instancetype': self._findattr(element, "instanceType"),
+ 'launchdatetime': self._findattr(element, "launchTime"),
+ 'availability': self._findattr(element,
+ "placement/availabilityZone"),
+ 'kernelid': self._findattr(element, "kernelId"),
+ 'ramdiskid': self._findattr(element, "ramdiskId")
+ }
+ )
+ return n
+
+ def _to_images(self, object):
+ return [ self._to_image(el)
+ for el in object.findall(
+ self._fixxpath('imagesSet/item')
+ ) ]
+
+ def _to_image(self, element):
+ n = NodeImage(id=self._findtext(element, 'imageId'),
+ name=self._findtext(element, 'imageLocation'),
+ driver=self.connection.driver)
+ return n
+
+ def list_nodes(self):
+ params = {'Action': 'DescribeInstances' }
+ nodes = self._to_nodes(
+ self.connection.request('/', params=params).object,
+ 'reservationSet/item/instancesSet/item')
+ return nodes
+
+ def list_sizes(self, location=None):
+ return [ NodeSize(driver=self.connection.driver, **i)
+ for i in self._instance_types.values() ]
+
+ def list_images(self, location=None):
+ params = {'Action': 'DescribeImages'}
+ images = self._to_images(
+ self.connection.request('/', params=params).object
+ )
+ return images
+
+ def create_security_group(self, name, description):
+ params = {'Action': 'CreateSecurityGroup',
+ 'GroupName': name,
+ 'GroupDescription': description}
+ return self.connection.request('/', params=params).object
+
+ def authorize_security_group_permissive(self, name):
+ results = []
+ params = {'Action': 'AuthorizeSecurityGroupIngress',
+ 'GroupName': name,
+ 'IpProtocol': 'tcp',
+ 'FromPort': '0',
+ 'ToPort': '65535',
+ 'CidrIp': '0.0.0.0/0'}
+ try:
+ results.append(
+ self.connection.request('/', params=params.copy()).object
+ )
+ except Exception, e:
+ if e.args[0].find("InvalidPermission.Duplicate") == -1:
+ raise e
+ params['IpProtocol'] = 'udp'
+
+ try:
+ results.append(
+ self.connection.request('/', params=params.copy()).object
+ )
+ except Exception, e:
+ if e.args[0].find("InvalidPermission.Duplicate") == -1:
+ raise e
+
+ params.update({'IpProtocol': 'icmp', 'FromPort': '-1', 'ToPort': '-1'})
+
+ try:
+ results.append(
+ self.connection.request('/', params=params.copy()).object
+ )
+ except Exception, e:
+ if e.args[0].find("InvalidPermission.Duplicate") == -1:
+ raise e
+ return results
+
+ def create_node(self, **kwargs):
+ """Create a new EC2 node
+
+ See L{NodeDriver.create_node} for more keyword args.
+ Reference: http://bit.ly/8ZyPSy [docs.amazonwebservices.com]
+
+ @keyword name: Name (unused by EC2)
+ @type name: C{str}
+
+ @keyword mincount: Minimum number of instances to launch
+ @type mincount: C{int}
+
+ @keyword maxcount: Maximum number of instances to launch
+ @type maxcount: C{int}
+
+ @keyword securitygroup: Name of security group
+ @type securitygroup: C{str}
+
+ @keyword keyname: The name of the key pair
+ @type keyname: C{str}
+
+ @keyword userdata: User data
+ @type userdata: C{str}
+ """
+ name = kwargs["name"]
+ image = kwargs["image"]
+ size = kwargs["size"]
+ params = {
+ 'Action': 'RunInstances',
+ 'ImageId': image.id,
+ 'MinCount': kwargs.get('mincount','1'),
+ 'MaxCount': kwargs.get('maxcount','1'),
+ 'InstanceType': size.id
+ }
+
+ if 'securitygroup' in kwargs:
+ params['SecurityGroup'] = kwargs['securitygroup']
+
+ if 'keyname' in kwargs:
+ params['KeyName'] = kwargs['keyname']
+
+ if 'userdata' in kwargs:
+ params['UserData'] = base64.b64encode(kwargs['userdata'])
+
+ object = self.connection.request('/', params=params).object
+ nodes = self._to_nodes(object, 'instancesSet/item')
+
+ if len(nodes) == 1:
+ return nodes[0]
+ else:
+ return nodes
+
+ def reboot_node(self, node):
+ """
+ Reboot the node by passing in the node object
+ """
+ params = {'Action': 'RebootInstances'}
+ params.update(self._pathlist('InstanceId', [node.id]))
+ res = self.connection.request('/', params=params).object
+ return self._get_boolean(res)
+
+ def destroy_node(self, node):
+ """
+ Destroy node by passing in the node object
+ """
+ params = {'Action': 'TerminateInstances'}
+ params.update(self._pathlist('InstanceId', [node.id]))
+ res = self.connection.request('/', params=params).object
+ return self._get_terminate_boolean(res)
+
+ def list_locations(self):
+ return [NodeLocation(0, 'Amazon US N. Virginia', 'US', self)]
+
+class EC2EUConnection(EC2Connection):
+
+ host = EC2_EU_WEST_HOST
+
+class EC2EUNodeDriver(EC2NodeDriver):
+
+ connectionCls = EC2EUConnection
+ _instance_types = EC2_EU_WEST_INSTANCE_TYPES
+ def list_locations(self):
+ return [NodeLocation(0, 'Amazon Europe Ireland', 'IE', self)]
+
+class EC2USWestConnection(EC2Connection):
+
+ host = EC2_US_WEST_HOST
+
+class EC2USWestNodeDriver(EC2NodeDriver):
+
+ connectionCls = EC2USWestConnection
+ _instance_types = EC2_US_WEST_INSTANCE_TYPES
+ def list_locations(self):
+ return [NodeLocation(0, 'Amazon US N. California', 'US', self)]
diff --git a/libcloud/drivers/gogrid.py b/libcloud/drivers/gogrid.py
new file mode 100644
index 0000000..264951d
--- /dev/null
+++ b/libcloud/drivers/gogrid.py
@@ -0,0 +1,234 @@
+# 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.
+# libcloud.org 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.
+"""
+GoGrid driver
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, ConnectionUserAndKey, Response, NodeDriver
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+import time
+import hashlib
+
+# 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
+
+HOST = 'api.gogrid.com'
+PORTS_BY_SECURITY = { True: 443, False: 80 }
+API_VERSION = '1.3'
+
+STATE = {
+ "Starting": NodeState.PENDING,
+ "On": NodeState.RUNNING,
+ "Off": NodeState.PENDING,
+ "Restarting": NodeState.REBOOTING,
+ "Saving": NodeState.PENDING,
+ "Restoring": NodeState.PENDING,
+}
+
+GOGRID_INSTANCE_TYPES = {'512MB': {'id': '512MB',
+ 'name': '512MB',
+ 'ram': 512,
+ 'disk': 30,
+ 'bandwidth': None,
+ 'price':0.095},
+ '1GB': {'id': '1GB',
+ 'name': '1GB',
+ 'ram': 1024,
+ 'disk': 60,
+ 'bandwidth': None,
+ 'price':0.19},
+ '2GB': {'id': '2GB',
+ 'name': '2GB',
+ 'ram': 2048,
+ 'disk': 120,
+ 'bandwidth': None,
+ 'price':0.38},
+ '4GB': {'id': '4GB',
+ 'name': '4GB',
+ 'ram': 4096,
+ 'disk': 240,
+ 'bandwidth': None,
+ 'price':0.76},
+ '8GB': {'id': '8GB',
+ 'name': '8GB',
+ 'ram': 8192,
+ 'disk': 480,
+ 'bandwidth': None,
+ 'price':1.52}}
+
+
+class GoGridResponse(Response):
+ def success(self):
+ if self.status == 403:
+ raise InvalidCredsException()
+ if not self.body:
+ return None
+ return json.loads(self.body)['status'] == 'success'
+
+ def parse_body(self):
+ if not self.body:
+ return None
+ return json.loads(self.body)
+
+ def parse_error(self):
+ if not self.object:
+ return None
+ return self.object['message']
+
+class GoGridConnection(ConnectionUserAndKey):
+
+ host = HOST
+ responseCls = GoGridResponse
+
+ def add_default_params(self, params):
+ params["api_key"] = self.user_id
+ params["v"] = API_VERSION
+ params["format"] = 'json'
+ params["sig"] = self.get_signature(self.user_id, self.key)
+
+ return params
+
+ def get_signature(self, key, secret):
+ """ create sig from md5 of key + secret + time """
+ m = hashlib.md5(key+secret+str(int(time.time())))
+ return m.hexdigest()
+
+class GoGridNode(Node):
+ # Generating uuid based on public ip to get around missing id on
+ # create_node in gogrid api
+ #
+ # Used public ip since it is not mutable and specified at create time,
+ # so uuid of node should not change after add is completed
+ def get_uuid(self):
+ return hashlib.sha1(
+ "%s:%d" % (self.public_ip,self.driver.type)
+ ).hexdigest()
+
+class GoGridNodeDriver(NodeDriver):
+
+ connectionCls = GoGridConnection
+ type = Provider.GOGRID
+ name = 'GoGrid API'
+
+ _instance_types = GOGRID_INSTANCE_TYPES
+
+ def get_state(self, element):
+ try:
+ return STATE[element['state']['name']]
+ except:
+ pass
+ return NodeState.UNKNOWN
+
+ def get_ip(self, element):
+ return element['ip']['ip']
+
+ def get_id(self,element):
+ return element['id']
+
+ def _to_node(self, element):
+ state = self.get_state(element)
+ ip = self.get_ip(element)
+ id = self.get_id(element)
+ n = GoGridNode(id=id,
+ name=element['name'],
+ state=state,
+ public_ip=[ip],
+ private_ip=[],
+ driver=self.connection.driver)
+ return n
+
+ def _to_image(self, element):
+ n = NodeImage(id=element['id'],
+ name=element['friendlyName'],
+ driver=self.connection.driver)
+ return n
+
+ def _to_images(self, object):
+ return [ self._to_image(el)
+ for el in object['list'] ]
+
+ def list_images(self, location=None):
+ images = self._to_images(
+ self.connection.request('/api/grid/image/list').object)
+ return images
+
+ def get_uuid(self, field):
+ uuid_str = "%s:%s" % (field,self.connection.user_id)
+ return hashlib.sha1(uuid_str).hexdigest()
+
+ def list_nodes(self):
+ res = self.server_list()
+ return [ self._to_node(el)
+ for el
+ in res['list'] ]
+
+ def reboot_node(self, node):
+ id = node.id
+ power = 'restart'
+ res = self.server_power(id, power)
+ if not res.success():
+ raise Exception(res.parse_error())
+ return True
+
+ def destroy_node(self, node):
+ id = node.id
+ res = self.server_delete(id)
+ if not res.success():
+ raise Exception(res.parse_error())
+ return True
+
+ def server_list(self):
+ return self.connection.request('/api/grid/server/list').object
+
+ def server_power(self, id, power):
+ # power in ['start', 'stop', 'restart']
+ params = {'id': id, 'power': power}
+ return self.connection.request("/api/grid/server/power", params)
+
+ def server_delete(self, id):
+ params = {'id': id}
+ return self.connection.request("/api/grid/server/delete", params)
+
+ def get_first_ip(self):
+ params = {'ip.state': 'Unassigned', 'ip.type':'public'}
+ object = self.connection.request("/api/grid/ip/list", params).object
+ return object['list'][0]['ip']
+
+ def list_sizes(self, location=None):
+ return [ NodeSize(driver=self.connection.driver, **i)
+ for i in self._instance_types.values() ]
+
+ def list_locations(self):
+ return [NodeLocation(0, "GoGrid Los Angeles", 'US', self)]
+
+ def create_node(self, **kwargs):
+ name = kwargs['name']
+ image = kwargs['image']
+ size = kwargs['size']
+ first_ip = self.get_first_ip()
+ params = {'name': name,
+ 'image': image.id,
+ 'description': kwargs.get('description',''),
+ 'server.ram': size.id,
+ 'ip':first_ip}
+
+ object = self.connection.request('/api/grid/server/add',
+ params=params).object
+ node = self._to_node(object['list'][0])
+
+ return node
diff --git a/libcloud/drivers/linode.py b/libcloud/drivers/linode.py
new file mode 100644
index 0000000..f5d5c33
--- /dev/null
+++ b/libcloud/drivers/linode.py
@@ -0,0 +1,439 @@
+# 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.
+# libcloud.org 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 <jsmith@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 <jsmith@linode.com> for your request.
+#
+"""
+Linode driver
+"""
+from libcloud.types import Provider, NodeState, InvalidCredsException
+from libcloud.base import ConnectionKey, Response
+from libcloud.base import NodeDriver, NodeSize, Node, NodeLocation
+from libcloud.base import NodeAuthPassword, NodeAuthSSHKey
+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
+
+
+# Base exception for problems arising from this driver
+class LinodeException(Exception):
+ 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".
+LINODE_API = "api.linode.com"
+
+# For beta accounts, change this to "/api/".
+LINODE_ROOT = "/"
+
+
+class LinodeResponse(Response):
+ # Wraps a Linode API HTTP response.
+
+ def __init__(self, response):
+ # Given a response object, slurp the information from it.
+ self.body = response.read()
+ self.status = response.status
+ self.headers = dict(response.getheaders())
+ self.error = response.reason
+ 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.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)
+ 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)
+ except:
+ # Assume invalid JSON, and use an error code unused by Linode API.
+ return (None, None, [self.invalid])
+
+ def parse_error(self):
+ # Obtain the errors from the response. Will always return a list.
+ try:
+ js = json.loads(self.body)
+ if "ERRORARRAY" not in js:
+ return [self.invalid]
+ return [self._make_excp(e) for e in js["ERRORARRAY"]]
+ except:
+ return [self.invalid]
+
+ def success(self):
+ # Does the response indicate success? If ERRORARRAY has more than one
+ # entry, we'll say no.
+ return len(self.errors) == 0
+
+ def _make_excp(self, error):
+ # Make an exception from an entry in ERRORARRAY.
+ if "ERRORCODE" not in error or "ERRORMESSAGE" not in error:
+ return None
+ if error["ERRORCODE"] == 4:
+ return InvalidCredsException(error["ERRORMESSAGE"])
+ return LinodeException(error["ERRORCODE"], error["ERRORMESSAGE"])
+
+
+class LinodeConnection(ConnectionKey):
+ # Wraps a Linode HTTPS connection, and passes along the connection key.
+ host = LINODE_API
+ responseCls = LinodeResponse
+ def add_default_params(self, params):
+ params["api_key"] = self.key
+ # Be explicit about this in case the default changes.
+ params["api_responseFormat"] = "json"
+ return params
+
+
+class LinodeNodeDriver(NodeDriver):
+ # The meat of Linode operations; the Node Driver.
+ type = Provider.LINODE
+ name = "Linode"
+ connectionCls = LinodeConnection
+
+ def __init__(self, key):
+ self.datacenter = None
+ NodeDriver.__init__(self, key)
+
+ # Converts Linode's state from DB to a NodeState constant.
+ # Some of these are lightly questionable.
+ LINODE_STATES = {
+ -2: NodeState.UNKNOWN, # Boot Failed
+ -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 (?)
+ 4: NodeState.UNKNOWN # Reserved
+ }
+
+ def list_nodes(self):
+ # List
+ # Provide a list of all nodes that this API key has access to.
+ 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.
+ 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.
+ 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
+
+ See L{NodeDriver.create_node} for more keyword args.
+
+ @keyword swap: Size of the swap partition in MB (128).
+ @type swap: C{number}
+
+ @keyword rsize: Size of the root partition (plan size - swap).
+ @type rsize: C{number}
+
+ @keyword kernel: A kernel ID from avail.kernels (Latest 2.6).
+ @type kernel: C{number}
+
+ @keyword payment: One of 1, 12, or 24; subscription length (1)
+ @type payment: C{number}
+
+ @keyword comment: Comments to store with the config
+ @type 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
+ #
+ # Datacenter logic:
+ #
+ # As Linode requires choosing a datacenter, a little logic is done.
+ #
+ # 1. If the API key in use has all its Linodes in one DC, that DC will
+ # be chosen (and can be overridden with linode_set_datacenter).
+ #
+ # 2. Otherwise (for both the "No Linodes" and "different DC" cases), a
+ # datacenter must explicitly be chosen using linode_set_datacenter.
+ #
+ # Please note that for safety, only 5 Linodes can be created per hour.
+
+ name = kwargs["name"]
+ chosen = kwargs["location"].id
+ image = kwargs["image"]
+ size = kwargs["size"]
+ auth = kwargs["auth"]
+
+ # 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 "payment" not in kwargs else str(kwargs["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
+ elif isinstance(auth, NodeAuthPassword):
+ root = auth.password
+
+ if not ssh and not root:
+ raise LinodeException(0xFB, "Need SSH key or root password")
+ if len(root) < 6:
+ raise LinodeException(0xFB, "Root password is too short")
+
+ # Swap size
+ try: swap = 128 if "swap" not in kwargs else int(kwargs["swap"])
+ except: raise LinodeException(0xFB, "Need an integer swap size")
+
+ # Root partition size
+ imagesize = (size.disk - swap) if "rsize" not in kwargs else \
+ int(kwargs["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
+ kernel = 60 if "kernel" not in kwargs else kwargs["kernel"]
+ params = { "api_action": "avail.kernels" }
+ kernels = self.connection.request(LINODE_ROOT, params=params).object
+ if kernel not in [z["KERNELID"] for z in kernels]:
+ raise LinodeException(0xFB, "Invalid kernel -- avail.kernels")
+
+ # Comments
+ comments = "Created by libcloud <http://www.libcloud.org>" if \
+ "comment" not in kwargs else kwargs["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"]:
+ if what in kwargs:
+ label[what] = kwargs[what]
+
+ # Step 1: linode.create
+ params = {
+ "api_action": "linode.create",
+ "DatacenterID": chosen,
+ "PlanID": size.id,
+ "PaymentTerm": payment
+ }
+ data = self.connection.request(LINODE_ROOT, params=params).object
+ linode = { "id": data["LinodeID"] }
+
+ # Step 2: linode.disk.createfromdistribution
+ if not root:
+ root = os.urandom(16).encode('hex')
+ 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(LINODE_ROOT, params=params).object
+ 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(LINODE_ROOT, params=params).object
+ linode["swapimage"] = data["DiskID"]
+
+ # Step 4: linode.config.create for main profile
+ disks = "%s,%s,,,,,,," % (linode["rootimage"], linode["swapimage"])
+ params = {
+ "api_action": "linode.config.create",
+ "LinodeID": linode["id"],
+ "KernelID": kernel,
+ "Label": label["lconfig"],
+ "Comments": comments,
+ "DiskList": disks
+ }
+ 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",
+ "LinodeID": linode["id"],
+ "ConfigID": linode["config"]
+ }
+ data = self.connection.request(LINODE_ROOT, params=params).object
+
+ # Make a node out of it and hand it back
+ params = { "api_action": "linode.list", "LinodeID": linode["id"] }
+ data = self.connection.request(LINODE_ROOT, params=params).object
+ 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'.
+ params = { "api_action": "avail.linodeplans" }
+ data = self.connection.request(LINODE_ROOT, params=params).object
+ 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, location=None):
+ # List Images
+ # Retrieve all available Linux distributions.
+ params = { "api_action": "avail.distributions" }
+ data = self.connection.request(LINODE_ROOT, params=params).object
+ distros = []
+ for obj in data:
+ i = NodeImage(id=obj["DISTRIBUTIONID"], name=obj["LABEL"],
+ driver=self.connection.driver)
+ distros.append(i)
+ return distros
+
+ def list_locations(self):
+ 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"]
+ )
+ nl.append(NodeLocation(dc["DATACENTERID"],
+ dc["LOCATION"],
+ country,
+ self))
+ return nl
+
+ def linode_set_datacenter(self, did):
+ # Set the datacenter for create requests.
+ #
+ # Create will try to guess, based on where all of the API key's
+ # Linodes are located; if they are all in one location, Create will
+ # make a new node there. If there are NO Linodes on the account or
+ # Linodes are in multiple locations, it is imperative to set this or
+ # creates will fail.
+ params = { "api_action": "avail.datacenters" }
+ data = self.connection.request(LINODE_ROOT, params=params).object
+ for dc 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 _to_node(self, obj):
+ # Convert a returned Linode instance into a Node instance.
+ lid = obj["LINODEID"]
+
+ # Get the IP addresses for a 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:
+ return None
+
+ public_ip = []
+ private_ip = []
+ for ip in req.object:
+ if ip["ISPUBLIC"]:
+ public_ip.append(ip["IPADDRESS"])
+ else:
+ private_ip.append(ip["IPADDRESS"])
+
+ n = Node(id=lid, name=obj["LABEL"],
+ state=self.LINODE_STATES[obj["STATUS"]], public_ip=public_ip,
+ private_ip=private_ip, driver=self.connection.driver)
+ n.extra = copy(obj)
+ return n
+
+ features = {"create_node": ["ssh_key", "password"]}
diff --git a/libcloud/drivers/rackspace.py b/libcloud/drivers/rackspace.py
new file mode 100644
index 0000000..5f81e95
--- /dev/null
+++ b/libcloud/drivers/rackspace.py
@@ -0,0 +1,308 @@
+# 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.
+# libcloud.org 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.
+"""
+Rackspace driver
+"""
+from libcloud.types import NodeState, InvalidCredsException, Provider
+from libcloud.base import ConnectionUserAndKey, Response, NodeDriver, Node
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+import os
+
+import base64
+import urlparse
+
+from xml.etree import ElementTree as ET
+from xml.parsers.expat import ExpatError
+
+NAMESPACE = 'http://docs.rackspacecloud.com/servers/api/v1.0'
+
+class RackspaceResponse(Response):
+
+ def success(self):
+ i = int(self.status)
+ return i >= 200 and i <= 299
+
+ def parse_body(self):
+ if not self.body:
+ return None
+ return ET.XML(self.body)
+
+ def parse_error(self):
+ # TODO: fixup, Rackspace only uses response codes really!
+ try:
+ object = ET.XML(self.body)
+ text = "; ".join([ err.text or ''
+ for err in
+ object.getiterator()
+ if err.text])
+ except ExpatError:
+ text = self.body
+ return '%s %s %s' % (self.status, self.error, text)
+
+
+class RackspaceConnection(ConnectionUserAndKey):
+ api_version = 'v1.0'
+ auth_host = 'auth.api.rackspacecloud.com'
+ responseCls = RackspaceResponse
+
+ def __init__(self, user_id, key, secure=True):
+ self.__host = None
+ self.path = None
+ self.token = None
+ super(RackspaceConnection, self).__init__(user_id, key, secure)
+
+ def add_default_headers(self, headers):
+ headers['X-Auth-Token'] = self.token;
+ headers['Accept'] = 'application/xml'
+ return headers
+
+ @property
+ def host(self):
+ """
+ Rackspace uses a separate host for API calls which is only provided
+ after an initial authentication request. If we haven't made that
+ request yet, do it here. Otherwise, just return the management host.
+
+ TODO: Fixup for when our token expires (!!!)
+ """
+ if not self.__host:
+ # Initial connection used for authentication
+ conn = self.conn_classes[self.secure](self.auth_host, self.port[self.secure])
+ conn.request(
+ method='GET',
+ url='/%s' % self.api_version,
+ headers={
+ 'X-Auth-User': self.user_id,
+ 'X-Auth-Key': self.key
+ }
+ )
+ resp = conn.getresponse()
+ headers = dict(resp.getheaders())
+ try:
+ self.token = headers['x-auth-token']
+ endpoint = headers['x-server-management-url']
+ except KeyError:
+ raise InvalidCredsException()
+
+ scheme, server, self.path, param, query, fragment = (
+ urlparse.urlparse(endpoint)
+ )
+ if scheme is "https" and self.secure is not 1:
+ # TODO: Custom exception (?)
+ raise InvalidCredsException()
+
+ # Set host to where we want to make further requests to;
+ # close auth conn
+ self.__host = server
+ conn.close()
+
+ return self.__host
+
+ def request(self, action, params={}, data='', headers={}, method='GET'):
+ # Due to first-run authentication request, we may not have a path
+ if self.path:
+ action = self.path + action
+ if method == "POST":
+ headers = {'Content-Type': 'application/xml; charset=UTF-8'}
+ if method == "GET":
+ params['cache-busting'] = os.urandom(8).encode('hex')
+ return super(RackspaceConnection, self).request(
+ action=action,
+ params=params, data=data,
+ method=method, headers=headers
+ )
+
+
+class RackspaceNodeDriver(NodeDriver):
+ """
+ Rackspace node driver.
+
+ Extra node attributes:
+ - password: root password, available after create.
+ - hostId: represents the host your cloud server runs on
+ - imageId: id of image
+ - flavorId: id of flavor
+ """
+ connectionCls = RackspaceConnection
+ type = Provider.RACKSPACE
+ name = 'Rackspace'
+
+ NODE_STATE_MAP = { 'BUILD': NodeState.PENDING,
+ 'ACTIVE': NodeState.RUNNING,
+ 'SUSPENDED': NodeState.TERMINATED,
+ 'QUEUE_RESIZE': NodeState.PENDING,
+ 'PREP_RESIZE': NodeState.PENDING,
+ 'RESCUE': NodeState.PENDING,
+ 'REBUILD': NodeState.PENDING,
+ 'REBOOT': NodeState.REBOOTING,
+ 'HARD_REBOOT': NodeState.REBOOTING}
+
+ def list_nodes(self):
+ return self.to_nodes(self.connection.request('/servers/detail').object)
+
+ def list_sizes(self, location=None):
+ return self.to_sizes(self.connection.request('/flavors/detail').object)
+
+ def list_images(self, location=None):
+ return self.to_images(self.connection.request('/images/detail').object)
+
+ def list_locations(self):
+ return [NodeLocation(0, "Rackspace DFW1", 'US', self)]
+
+ def create_node(self, **kwargs):
+ """Create a new rackspace node
+
+ See L{NodeDriver.create_node} for more keyword args.
+ @keyword metadata: Key/Value metadata to associate with a node
+ @type metadata: C{dict}
+
+ @keyword file: File Path => File contents to create on the node
+ @type file: C{dict}
+ """
+ name = kwargs['name']
+ image = kwargs['image']
+ size = kwargs['size']
+ server_elm = ET.Element(
+ 'server',
+ {'xmlns': NAMESPACE,
+ 'name': name,
+ 'imageId': str(image.id),
+ 'flavorId': str(size.id)}
+ )
+
+ metadata_elm = self._metadata_to_xml(kwargs.get("metadata", {}))
+ if metadata_elm:
+ server_elm.append(metadata_elm)
+
+ files_elm = self._files_to_xml(kwargs.get("files", {}))
+ if files_elm:
+ server_elm.append(files_elm)
+
+ resp = self.connection.request("/servers",
+ method='POST',
+ data=ET.tostring(server_elm))
+ return self._to_node(resp.object)
+
+ def _metadata_to_xml(self, metadata):
+ if len(metadata) == 0:
+ return None
+
+ metadata_elm = ET.Element('metadata')
+ for k, v in metadata.items():
+ meta_elm = ET.SubElement(metadata_elm, 'meta', {'key': str(k) })
+ meta_elm.text = str(v)
+
+ return metadata_elm
+
+ def _files_to_xml(self, files):
+ if len(files) == 0:
+ return None
+
+ personality_elm = ET.Element('personality')
+ for k, v in files.items():
+ file_elm = ET.SubElement(personality_elm,
+ 'file',
+ {'path': str(k)})
+ file_elm.text = base64.b64encode(v)
+
+ return personality_elm
+
+ def reboot_node(self, node):
+ # TODO: Hard Reboots should be supported too!
+ resp = self._node_action(node, ['reboot', ('type', 'SOFT')])
+ return resp.status == 202
+
+ def destroy_node(self, node):
+ uri = '/servers/%s' % (node.id)
+ resp = self.connection.request(uri, method='DELETE')
+ return resp.status == 202
+
+ def _node_action(self, node, body):
+ if isinstance(body, list):
+ attr = ' '.join(['%s="%s"' % (item[0], item[1])
+ for item in body[1:]])
+ body = '<%s xmlns="%s" %s/>' % (body[0], NAMESPACE, attr)
+ uri = '/servers/%s/action' % (node.id)
+ resp = self.connection.request(uri, method='POST', data=body)
+ return resp
+
+ def to_nodes(self, object):
+ node_elements = self._findall(object, 'server')
+ return [ self._to_node(el) for el in node_elements ]
+
+ def _fixxpath(self, xpath):
+ # ElementTree wants namespaces in its xpaths, so here we add them.
+ return "/".join(["{%s}%s" % (NAMESPACE, e) for e in xpath.split("/")])
+
+ def _findall(self, element, xpath):
+ return element.findall(self._fixxpath(xpath))
+
+ def _to_node(self, el):
+ def get_ips(el):
+ return [ip.get('addr') for ip in el]
+
+ def get_meta_dict(el):
+ d = {}
+ for meta in el:
+ d[meta.get('key')] = meta.text
+ return d
+
+ public_ip = get_ips(self._findall(el,
+ 'addresses/public/ip'))
+ private_ip = get_ips(self._findall(el,
+ 'addresses/private/ip'))
+ metadata = get_meta_dict(self._findall(el, 'metadata/meta'))
+
+ n = Node(id=el.get('id'),
+ name=el.get('name'),
+ state=el.get('status'),
+ public_ip=public_ip,
+ private_ip=private_ip,
+ driver=self.connection.driver,
+ extra={
+ 'password': el.get('adminPass'),
+ 'hostId': el.get('hostId'),
+ 'imageId': el.get('imageId'),
+ 'flavorId': el.get('flavorId'),
+ 'metadata': metadata,
+ })
+ return n
+
+ def to_sizes(self, object):
+ elements = self._findall(object, 'flavor')
+ return [ self._to_size(el) for el in elements ]
+
+ def _to_size(self, el):
+ s = NodeSize(id=el.get('id'),
+ name=el.get('name'),
+ ram=int(el.get('ram')),
+ disk=int(el.get('disk')),
+ bandwidth=None, # XXX: needs hardcode
+ price=None, # XXX: needs hardcode,
+ driver=self.connection.driver)
+ return s
+
+ def to_images(self, object):
+ elements = self._findall(object, "image")
+ return [ self._to_image(el)
+ for el in elements
+ if el.get('status') == 'ACTIVE' ]
+
+ def _to_image(self, el):
+ i = NodeImage(id=el.get('id'),
+ name=el.get('name'),
+ driver=self.connection.driver,
+ extra={'serverId': el.get('serverId')})
+ return i
diff --git a/libcloud/drivers/rimuhosting.py b/libcloud/drivers/rimuhosting.py
new file mode 100644
index 0000000..0aebd7e
--- /dev/null
+++ b/libcloud/drivers/rimuhosting.py
@@ -0,0 +1,294 @@
+# 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.
+# libcloud.org 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.
+# Copyright 2009 RedRata Ltd
+"""
+RimuHosting Driver
+"""
+from libcloud.types import Provider, NodeState, InvalidCredsException
+from libcloud.base import ConnectionKey, Response, NodeAuthPassword
+from libcloud.base import NodeDriver, NodeSize, Node, NodeLocation
+from libcloud.base import NodeImage
+
+# 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
+
+# Defaults
+API_CONTEXT = '/r'
+API_HOST = 'api.rimuhosting.com'
+API_PORT = (80,443)
+API_SECURE = True
+
+class RimuHostingException(Exception):
+ def __str__(self):
+ return self.args[0]
+
+ def __repr__(self):
+ return "<RimuHostingException '%s'>" % (self.args[0])
+
+class RimuHostingResponse(Response):
+ def __init__(self, response):
+ self.body = response.read()
+ self.status = response.status
+ self.headers = dict(response.getheaders())
+ self.error = response.reason
+
+ if self.success():
+ self.object = self.parse_body()
+
+ def success(self):
+ if self.status == 403:
+ raise InvalidCredsException()
+ return True
+ def parse_body(self):
+ try:
+ js = json.loads(self.body)
+ if js[js.keys()[0]]['response_type'] == "ERROR":
+ raise RimuHostingException(
+ js[js.keys()[0]]['human_readable_message']
+ )
+ return js[js.keys()[0]]
+ except ValueError:
+ raise RimuHostingException('Could not parse body: %s'
+ % (self.body))
+ except KeyError:
+ raise RimuHostingException('Could not parse body: %s'
+ % (self.body))
+
+class RimuHostingConnection(ConnectionKey):
+
+ api_context = API_CONTEXT
+ host = API_HOST
+ port = API_PORT
+ responseCls = RimuHostingResponse
+
+ def __init__(self, key, secure=True):
+ # override __init__ so that we can set secure of False for testing
+ ConnectionKey.__init__(self,key,secure)
+
+ def add_default_headers(self, headers):
+ # We want JSON back from the server. Could be application/xml
+ # (but JSON is better).
+ headers['Accept'] = 'application/json'
+ # Must encode all data as json, or override this header.
+ headers['Content-Type'] = 'application/json'
+
+ headers['Authorization'] = 'rimuhosting apikey=%s' % (self.key)
+ return headers;
+
+ def request(self, action, params={}, data='', headers={}, method='GET'):
+ # Override this method to prepend the api_context
+ return ConnectionKey.request(self, self.api_context + action,
+ params, data, headers, method)
+
+class RimuHostingNodeDriver(NodeDriver):
+ type = Provider.RIMUHOSTING
+ name = 'RimuHosting'
+ connectionCls = RimuHostingConnection
+
+ def __init__(self, key, host=API_HOST, port=API_PORT,
+ api_context=API_CONTEXT, secure=API_SECURE):
+ # Pass in some extra vars so that
+ self.key = key
+ self.secure = secure
+ self.connection = self.connectionCls(key ,secure)
+ self.connection.host = host
+ self.connection.api_context = api_context
+ self.connection.port = port
+ self.connection.driver = self
+ self.connection.connect()
+
+ def _order_uri(self, node,resource):
+ # Returns the order uri with its resourse appended.
+ return "/orders/%s/%s" % (node.id,resource)
+
+ # TODO: Get the node state.
+ def _to_node(self, order):
+ n = Node(id=order['slug'],
+ name=order['domain_name'],
+ state=NodeState.RUNNING,
+ public_ip=(
+ [order['allocated_ips']['primary_ip']]
+ + order['allocated_ips']['secondary_ips']
+ ),
+ private_ip=[],
+ driver=self.connection.driver,
+ extra={'order_oid': order['order_oid']})
+ return n
+
+ def _to_size(self,plan):
+ return NodeSize(
+ id=plan['pricing_plan_code'],
+ name=plan['pricing_plan_description'],
+ ram=plan['minimum_memory_mb'],
+ disk=plan['minimum_disk_gb'],
+ bandwidth=plan['minimum_data_transfer_allowance_gb'],
+ price=plan['monthly_recurring_amt']['amt_usd'],
+ driver=self.connection.driver
+ )
+
+ def _to_image(self,image):
+ return NodeImage(id=image['distro_code'],
+ name=image['distro_description'],
+ driver=self.connection.driver)
+
+ def list_sizes(self, location=None):
+ # Returns a list of sizes (aka plans)
+ # Get plans. Note this is really just for libcloud.
+ # We are happy with any size.
+ if location == None:
+ location = '';
+ else:
+ location = ";dc_location=%s" % (location.id)
+
+ res = self.connection.request('/pricing-plans;server-type=VPS%s' % (location)).object
+ return map(lambda x : self._to_size(x), res['pricing_plan_infos'])
+
+ def list_nodes(self):
+ # Returns a list of Nodes
+ # Will only include active ones.
+ res = self.connection.request('/orders;include_inactive=N').object
+ return map(lambda x : self._to_node(x), res['about_orders'])
+
+ def list_images(self, location=None):
+ # Get all base images.
+ # TODO: add other image sources. (Such as a backup of a VPS)
+ # All Images are available for use at all locations
+ res = self.connection.request('/distributions').object
+ return map(lambda x : self._to_image(x), res['distro_infos'])
+
+ def reboot_node(self, node):
+ # Reboot
+ # PUT the state of RESTARTING to restart a VPS.
+ # All data is encoded as JSON
+ data = {'reboot_request':{'running_state':'RESTARTING'}}
+ uri = self._order_uri(node,'vps/running-state')
+ self.connection.request(uri,data=json.dumps(data),method='PUT')
+ # XXX check that the response was actually successful
+ return True
+
+ def destroy_node(self, node):
+ # Shutdown a VPS.
+ uri = self._order_uri(node,'vps')
+ self.connection.request(uri,method='DELETE')
+ # XXX check that the response was actually successful
+ return True
+
+ def create_node(self, **kwargs):
+ # Creates a RimuHosting instance
+ #
+ # name Must be a FQDN. e.g example.com.
+ # image NodeImage from list_images
+ # size NodeSize from list_sizes
+ #
+ # Keyword arguements supported:
+ #
+ # billing_oid If not set, a billing method is automatically
+ # picked.
+ #
+ # host_server_oid The host server to set the VPS up on.
+ # vps_order_oid_to_clone Clone another VPS to use as the image
+ # for the new VPS.
+ #
+ # num_ips = 1 Number of IPs to allocate. Defaults to 1.
+ # extra_ip_reason Reason for needing the extra IPS.
+ #
+ # memory_mb Memory to allocate to the VPS.
+ # disk_space_mb Diskspace to allocate to the VPS.
+ # Defaults to 4096 (4GB).
+ # disk_space_2_mb Secondary disk size allocation.
+ # Disabled by default.
+ #
+ # pricing_plan_code Plan from list_sizes
+ #
+ # control_panel Control panel to install on the VPS.
+ #
+ #
+ # Note we don't do much error checking in this because we
+ # expect the API to error out if there is a problem.
+ name = kwargs['name']
+ image = kwargs['image']
+ size = kwargs['size']
+
+ data = {
+ 'instantiation_options':{
+ 'domain_name': name, 'distro': image.id
+ },
+ 'pricing_plan_code': size.id,
+ }
+
+ if kwargs.has_key('control_panel'):
+ data['instantiation_options']['control_panel'] = kwargs['control_panel']
+
+ if kwargs.has_key('auth'):
+ auth = kwargs['auth']
+ if not isinstance(auth, NodeAuthPassword):
+ raise ValueError('auth must be of NodeAuthPassword type')
+ data['instantiation_options']['password'] = auth.password
+
+ if kwargs.has_key('billing_oid'):
+ #TODO check for valid oid.
+ data['billing_oid'] = kwargs['billing_oid']
+
+ if kwargs.has_key('host_server_oid'):
+ data['host_server_oid'] = kwargs['host_server_oid']
+
+ if kwargs.has_key('vps_order_oid_to_clone'):
+ data['vps_order_oid_to_clone'] = kwargs['vps_order_oid_to_clone']
+
+ if kwargs.has_key('num_ips') and int(kwargs['num_ips']) > 1:
+ if not kwargs.has_key('extra_ip_reason'):
+ raise RimuHostingException('Need an reason for having an extra IP')
+ else:
+ if not data.has_key('ip_request'):
+ data['ip_request'] = {}
+ data['ip_request']['num_ips'] = int(kwargs['num_ips'])
+ data['ip_request']['extra_ip_reason'] = kwargs['extra_ip_reason']
+
+ if kwargs.has_key('memory_mb'):
+ if not data.has_key('vps_parameters'):
+ data['vps_parameters'] = {}
+ data['vps_parameters']['memory_mb'] = kwargs['memory_mb']
+
+ if kwargs.has_key('disk_space_mb'):
+ if not data.has_key('vps_parameters'):
+ data['vps_parameters'] = {}
+ data['vps_parameters']['disk_space_mb'] = kwargs['disk_space_mb']
+
+ if kwargs.has_key('disk_space_2_mb'):
+ if not data.has_key('vps_parameters'):
+ data['vps_parameters'] = {}
+ data['vps_parameters']['disk_space_2_mb'] = kwargs['disk_space_2_mb']
+
+ res = self.connection.request(
+ '/orders/new-vps',
+ method='POST',
+ data=json.dumps({"new-vps":data})
+ ).object
+ node = self._to_node(res['about_order'])
+ node.extra['password'] = res['new_order_request']['instantiation_options']['password']
+ return node
+
+ def list_locations(self):
+ return [
+ NodeLocation('DCAUCKLAND', "RimuHosting Auckland", 'NZ', self),
+ NodeLocation('DCDALLAS', "RimuHosting Dallas", 'US', self),
+ NodeLocation('DCLONDON', "RimuHosting London", 'GB', self),
+ NodeLocation('DCSYDNEY', "RimuHosting Sydney", 'AU', self),
+ ]
+
+ features = {"create_node": ["password"]}
+
diff --git a/libcloud/drivers/slicehost.py b/libcloud/drivers/slicehost.py
new file mode 100644
index 0000000..def2335
--- /dev/null
+++ b/libcloud/drivers/slicehost.py
@@ -0,0 +1,226 @@
+# 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.
+# libcloud.org 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.
+"""
+Slicehost Driver
+"""
+from libcloud.types import NodeState, Provider
+from libcloud.base import ConnectionKey, Response, NodeDriver, Node
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+import base64
+import struct
+import socket
+from xml.etree import ElementTree as ET
+from xml.parsers.expat import ExpatError
+
+class SlicehostResponse(Response):
+
+ def parse_body(self):
+ if not self.body:
+ return None
+ return ET.XML(self.body)
+
+ def parse_error(self):
+ try:
+ object = ET.XML(self.body)
+ return "; ".join([ err.text
+ for err in
+ object.findall('error') ])
+ except ExpatError:
+ return self.body
+
+
+class SlicehostConnection(ConnectionKey):
+
+ host = 'api.slicehost.com'
+ responseCls = SlicehostResponse
+
+ def add_default_headers(self, headers):
+ headers['Authorization'] = ('Basic %s'
+ % (base64.b64encode('%s:' % self.key)))
+ return headers
+
+
+class SlicehostNodeDriver(NodeDriver):
+
+ connectionCls = SlicehostConnection
+
+ type = Provider.SLICEHOST
+ name = 'Slicehost'
+
+ NODE_STATE_MAP = { 'active': NodeState.RUNNING,
+ 'build': NodeState.PENDING,
+ 'reboot': NodeState.REBOOTING,
+ 'hard_reboot': NodeState.REBOOTING,
+ 'terminated': NodeState.TERMINATED }
+
+ def list_nodes(self):
+ return self._to_nodes(self.connection.request('/slices.xml').object)
+
+ def list_sizes(self, location=None):
+ return self._to_sizes(self.connection.request('/flavors.xml').object)
+
+ def list_images(self, location=None):
+ return self._to_images(self.connection.request('/images.xml').object)
+
+ def list_locations(self):
+ return [
+ NodeLocation(0, 'Slicehost St. Louis (STL-A)', 'US', self),
+ NodeLocation(0, 'Slicehost St. Louis (STL-B)', 'US', self),
+ NodeLocation(0, 'Slicehost Dallas-Fort Worth (DFW-1)', 'US', self)
+ ]
+
+ def create_node(self, **kwargs):
+ name = kwargs['name']
+ image = kwargs['image']
+ size = kwargs['size']
+ uri = '/slices.xml'
+
+ # create a slice obj
+ root = ET.Element('slice')
+ el_name = ET.SubElement(root, 'name')
+ el_name.text = name
+ flavor_id = ET.SubElement(root, 'flavor-id')
+ flavor_id.text = str(size.id)
+ image_id = ET.SubElement(root, 'image-id')
+ image_id.text = str(image.id)
+ xml = ET.tostring(root)
+
+ node = self._to_nodes(
+ self.connection.request(
+ uri,
+ method='POST',
+ data=xml,
+ headers={'Content-Type': 'application/xml'}
+ ).object
+ )[0]
+ return node
+
+ def reboot_node(self, node):
+ """Reboot the node by passing in the node object"""
+
+ # 'hard' could bubble up as kwarg depending on how reboot_node
+ # turns out. Defaulting to soft reboot.
+ #hard = False
+ #reboot = self.api.hard_reboot if hard else self.api.reboot
+ #expected_status = 'hard_reboot' if hard else 'reboot'
+
+ uri = '/slices/%s/reboot.xml' % (node.id)
+ node = self._to_nodes(
+ self.connection.request(uri, method='PUT').object
+ )[0]
+ return node.state == NodeState.REBOOTING
+
+ def destroy_node(self, node):
+ """Destroys the node
+
+ Requires 'Allow Slices to be deleted or rebuilt from the API' to be
+ ticked at https://manage.slicehost.com/api, otherwise returns::
+ <errors>
+ <error>You must enable slice deletes in the SliceManager</error>
+ <error>Permission denied</error>
+ </errors>
+ """
+ uri = '/slices/%s/destroy.xml' % (node.id)
+ ret = self.connection.request(uri, method='PUT')
+ return True
+
+ def _to_nodes(self, object):
+ if object.tag == 'slice':
+ return [ self._to_node(object) ]
+ node_elements = object.findall('slice')
+ return [ self._to_node(el) for el in node_elements ]
+
+ def _to_node(self, element):
+
+ attrs = [ 'name', 'image-id', 'progress', 'id', 'bw-out', 'bw-in',
+ 'flavor-id', 'status', 'ip-address' ]
+
+ node_attrs = {}
+ for attr in attrs:
+ node_attrs[attr] = element.findtext(attr)
+
+ # slicehost does not determine between public and private, so we
+ # have to figure it out
+ public_ip = element.findtext('ip-address')
+ private_ip = None
+ for addr in element.findall('addresses/address'):
+ ip = addr.text
+ try:
+ socket.inet_aton(ip)
+ except socket.error:
+ # not a valid ip
+ continue
+ if self._is_private_subnet(ip):
+ private_ip = ip
+ else:
+ public_ip = ip
+
+ try:
+ state = self.NODE_STATE_MAP[element.findtext('status')]
+ except:
+ state = NodeState.UNKNOWN
+
+ n = Node(id=element.findtext('id'),
+ name=element.findtext('name'),
+ state=state,
+ public_ip=[public_ip],
+ private_ip=[private_ip],
+ driver=self.connection.driver)
+ return n
+
+ def _to_sizes(self, object):
+ if object.tag == 'flavor':
+ return [ self._to_size(object) ]
+ elements = object.findall('flavor')
+ return [ self._to_size(el) for el in elements ]
+
+ def _to_size(self, element):
+ s = NodeSize(id=int(element.findtext('id')),
+ name=str(element.findtext('name')),
+ ram=int(element.findtext('ram')),
+ disk=None, # XXX: needs hardcode
+ bandwidth=None, # XXX: needs hardcode
+ price=float(element.findtext('price'))/(100*24*30),
+ driver=self.connection.driver)
+ return s
+
+ def _to_images(self, object):
+ if object.tag == 'image':
+ return [ self._to_image(object) ]
+ elements = object.findall('image')
+ return [ self._to_image(el) for el in elements ]
+
+ def _to_image(self, element):
+ i = NodeImage(id=int(element.findtext('id')),
+ name=str(element.findtext('name')),
+ driver=self.connection.driver)
+ return i
+
+
+ def _is_private_subnet(self, ip):
+ priv_subnets = [ {'subnet': '10.0.0.0', 'mask': '255.0.0.0'},
+ {'subnet': '172.16.0.0', 'mask': '172.16.0.0'},
+ {'subnet': '192.168.0.0', 'mask': '192.168.0.0'} ]
+
+ ip = struct.unpack('I',socket.inet_aton(ip))[0]
+
+ for network in priv_subnets:
+ subnet = struct.unpack('I',socket.inet_aton(network['subnet']))[0]
+ mask = struct.unpack('I',socket.inet_aton(network['mask']))[0]
+
+ if (ip & mask) == (subnet & mask):
+ return True
+
+ return False
diff --git a/libcloud/drivers/vcloud.py b/libcloud/drivers/vcloud.py
new file mode 100644
index 0000000..1079b0f
--- /dev/null
+++ b/libcloud/drivers/vcloud.py
@@ -0,0 +1,587 @@
+# 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.
+# libcloud.org 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.
+"""
+vmware VCloud driver.
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver
+from libcloud.base import NodeSize, NodeImage, NodeAuthPassword, NodeLocation
+
+import base64
+import httplib
+import time
+from urlparse import urlparse
+from xml.etree import ElementTree as ET
+from xml.parsers.expat import ExpatError
+
+"""
+From vcloud api "The VirtualQuantity element defines the number of MB
+of memory. This should be either 512 or a multiple of 1024 (1 GB)."
+"""
+VIRTUAL_MEMORY_VALS = [512] + [1024 * i for i in range(1,9)]
+
+DEFAULT_TASK_COMPLETION_TIMEOUT = 600
+
+def get_url_path(url):
+ return urlparse(url.strip()).path
+
+def fixxpath(root, xpath):
+ """ElementTree wants namespaces in its xpaths, so here we add them."""
+ namespace, root_tag = root.tag[1:].split("}", 1)
+ fixed_xpath = "/".join(["{%s}%s" % (namespace, e)
+ for e in xpath.split("/")])
+ return fixed_xpath
+
+class InstantiateVAppXML(object):
+
+ def __init__(self, name, template, net_href, cpus, memory,
+ password=None, row=None, group=None):
+ self.name = name
+ self.template = template
+ self.net_href = net_href
+ self.cpus = cpus
+ self.memory = memory
+ self.password = password
+ self.row = row
+ self.group = group
+
+ self._build_xmltree()
+
+ def tostring(self):
+ return ET.tostring(self.root)
+
+ def _build_xmltree(self):
+ self.root = self._make_instantiation_root()
+
+ self._add_vapp_template(self.root)
+ instantionation_params = ET.SubElement(self.root,
+ "InstantiationParams")
+
+ product = self._make_product_section(instantionation_params)
+ virtual_hardware = self._make_virtual_hardware(instantionation_params)
+ network_config_section = ET.SubElement(instantionation_params,
+ "NetworkConfigSection")
+
+ network_config = ET.SubElement(network_config_section,
+ "NetworkConfig")
+ self._add_network_association(network_config)
+
+ def _make_instantiation_root(self):
+ return ET.Element(
+ "InstantiateVAppTemplateParams",
+ {'name': self.name,
+ 'xml:lang': 'en',
+ 'xmlns': "http://www.vmware.com/vcloud/v0.8",
+ 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"}
+ )
+
+ def _add_vapp_template(self, parent):
+ return ET.SubElement(
+ parent,
+ "VAppTemplate",
+ {'href': self.template}
+ )
+
+ def _make_product_section(self, parent):
+ prod_section = ET.SubElement(
+ parent,
+ "ProductSection",
+ {'xmlns:q1': "http://www.vmware.com/vcloud/v0.8",
+ 'xmlns:ovf': "http://schemas.dmtf.org/ovf/envelope/1"}
+ )
+
+ if self.password:
+ self._add_property(prod_section, 'password', self.password)
+
+ if self.row:
+ self._add_property(prod_section, 'row', self.row)
+
+ if self.group:
+ self._add_property(prod_section, 'group', self.group)
+
+ return prod_section
+
+ def _add_property(self, parent, ovfkey, ovfvalue):
+ return ET.SubElement(
+ parent,
+ "Property",
+ {'xmlns': 'http://schemas.dmtf.org/ovf/envelope/1',
+ 'ovf:key': ovfkey,
+ 'ovf:value': ovfvalue}
+ )
+
+ def _make_virtual_hardware(self, parent):
+ vh = ET.SubElement(
+ parent,
+ "VirtualHardwareSection",
+ {'xmlns:q1': "http://www.vmware.com/vcloud/v0.8"}
+ )
+
+ self._add_cpu(vh)
+ self._add_memory(vh)
+
+ return vh
+
+ def _add_cpu(self, parent):
+ cpu_item = ET.SubElement(
+ parent,
+ "Item",
+ {'xmlns': "http://schemas.dmtf.org/ovf/envelope/1"}
+ )
+ self._add_instance_id(cpu_item, '1')
+ self._add_resource_type(cpu_item, '3')
+ self._add_virtual_quantity(cpu_item, self.cpus)
+
+ return cpu_item
+
+ def _add_memory(self, parent):
+ mem_item = ET.SubElement(
+ parent,
+ "Item",
+ {'xmlns': "http://schemas.dmtf.org/ovf/envelope/1"}
+ )
+ self._add_instance_id(mem_item, '2')
+ self._add_resource_type(mem_item, '4')
+ self._add_virtual_quantity(mem_item, self.memory)
+
+ return mem_item
+
+ def _add_instance_id(self, parent, id):
+ elm = ET.SubElement(
+ parent,
+ "InstanceID",
+ {'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData'}
+ )
+ elm.text = id
+ return elm
+
+ def _add_resource_type(self, parent, type):
+ elm = ET.SubElement(
+ parent,
+ "ResourceType",
+ {'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData'}
+ )
+ elm.text = type
+ return elm
+
+ def _add_virtual_quantity(self, parent, amount):
+ elm = ET.SubElement(
+ parent,
+ "VirtualQuantity",
+ {'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData'}
+ )
+ elm.text = amount
+ return elm
+
+ def _add_network_association(self, parent):
+ return ET.SubElement(
+ parent,
+ "NetworkAssociation",
+ {'href': self.net_href}
+ )
+
+class VCloudResponse(Response):
+
+ def parse_body(self):
+ if not self.body:
+ return None
+ try:
+ return ET.XML(self.body)
+ except ExpatError, e:
+ raise Exception("%s: %s" % (e, self.parse_error()))
+
+ def parse_error(self):
+ return self.error
+
+ def success(self):
+ return self.status in (httplib.OK, httplib.CREATED,
+ httplib.NO_CONTENT, httplib.ACCEPTED)
+
+class VCloudConnection(ConnectionUserAndKey):
+
+ responseCls = VCloudResponse
+ token = None
+ host = None
+
+ def request(self, *args, **kwargs):
+ self._get_auth_token()
+ return super(VCloudConnection, self).request(*args, **kwargs)
+
+ def check_org(self):
+ # the only way to get our org is by logging in.
+ self._get_auth_token()
+
+ def _get_auth_headers(self):
+ """Some providers need different headers than others"""
+ return {
+ 'Authorization':
+ "Basic %s"
+ % base64.b64encode('%s:%s' % (self.user_id, self.key)),
+ 'Content-Length': 0
+ }
+
+ def _get_auth_token(self):
+ if not self.token:
+ conn = self.conn_classes[self.secure](self.host,
+ self.port[self.secure])
+ conn.request(method='POST', url='/api/v0.8/login',
+ headers=self._get_auth_headers())
+
+ resp = conn.getresponse()
+ headers = dict(resp.getheaders())
+ body = ET.XML(resp.read())
+
+ try:
+ self.token = headers['set-cookie']
+ except KeyError:
+ raise InvalidCredsException()
+
+ self.driver.org = get_url_path(
+ body.find(fixxpath(body, 'Org')).get('href')
+ )
+
+ def add_default_headers(self, headers):
+ headers['Cookie'] = self.token
+ return headers
+
+class VCloudNodeDriver(NodeDriver):
+ type = Provider.VCLOUD
+ name = "vCloud"
+ connectionCls = VCloudConnection
+ org = None
+ _vdcs = None
+
+ NODE_STATE_MAP = {'0': NodeState.PENDING,
+ '1': NodeState.PENDING,
+ '2': NodeState.PENDING,
+ '3': NodeState.PENDING,
+ '4': NodeState.RUNNING}
+
+ @property
+ def vdcs(self):
+ if not self._vdcs:
+ self.connection.check_org() # make sure the org is set.
+ res = self.connection.request(self.org)
+ self._vdcs = [
+ get_url_path(i.get('href'))
+ for i
+ in res.object.findall(fixxpath(res.object, "Link"))
+ if i.get('type') == 'application/vnd.vmware.vcloud.vdc+xml'
+ ]
+
+ return self._vdcs
+
+ @property
+ def networks(self):
+ networks = []
+ for vdc in self.vdcs:
+ res = self.connection.request(vdc).object
+ networks.extend(
+ [network
+ for network in res.findall(
+ fixxpath(res, "AvailableNetworks/Network")
+ )]
+ )
+
+ return networks
+
+ def _to_image(self, image):
+ image = NodeImage(id=image.get('href'),
+ name=image.get('name'),
+ driver=self.connection.driver)
+ return image
+
+ def _to_node(self, name, elm):
+ state = self.NODE_STATE_MAP[elm.get('status')]
+ public_ips = []
+ private_ips = []
+
+ # Following code to find private IPs works for Terremark
+ connections = elm.findall('{http://schemas.dmtf.org/ovf/envelope/1}NetworkConnectionSection/{http://www.vmware.com/vcloud/v0.8}NetworkConnection')
+ for connection in connections:
+ ips = [ip.text
+ for ip
+ in connection.findall(fixxpath(elm, "IpAddress"))]
+ if connection.get('Network') == 'Internal':
+ private_ips.extend(ips)
+ else:
+ public_ips.extend(ips)
+
+ node = Node(id=elm.get('href'),
+ name=name,
+ state=state,
+ public_ip=public_ips,
+ private_ip=private_ips,
+ driver=self.connection.driver)
+
+ return node
+
+ def _get_catalog_hrefs(self):
+ res = self.connection.request(self.org)
+ catalogs = [
+ get_url_path(i.get('href'))
+ for i in res.object.findall(fixxpath(res.object, "Link"))
+ if i.get('type') == 'application/vnd.vmware.vcloud.catalog+xml'
+ ]
+
+ return catalogs
+
+ def _wait_for_task_completion(self, task_href,
+ timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
+ start_time = time.time()
+ res = self.connection.request(task_href)
+ status = res.object.get('status')
+ while status != 'success':
+ if status == 'error':
+ raise Exception("Error status returned by task %s."
+ % task_href)
+ if status == 'canceled':
+ raise Exception("Canceled status returned by task %s."
+ % task_href)
+ if (time.time() - start_time >= timeout):
+ raise Exception("Timeout while waiting for task %s."
+ % task_href)
+ time.sleep(5)
+ res = self.connection.request(task_href)
+ status = res.object.get('status')
+
+ def destroy_node(self, node):
+ node_path = get_url_path(node.id)
+ # blindly poweroff node, it will throw an exception if already off
+ try:
+ res = self.connection.request('%s/power/action/poweroff'
+ % node_path,
+ method='POST')
+ self._wait_for_task_completion(res.object.get('href'))
+ except Exception, e:
+ pass
+
+ try:
+ res = self.connection.request('%s/action/undeploy' % node_path,
+ method='POST')
+ self._wait_for_task_completion(res.object.get('href'))
+ except ExpatError:
+ # The undeploy response is malformed XML atm.
+ # We can remove this whent he providers fix the problem.
+ pass
+ except Exception, e:
+ # Some vendors don't implement undeploy at all yet,
+ # so catch this and move on.
+ pass
+
+ res = self.connection.request(node_path, method='DELETE')
+ return res.status == 202
+
+ def reboot_node(self, node):
+ res = self.connection.request('%s/power/action/reset'
+ % get_url_path(node.id),
+ method='POST')
+ return res.status == 202 or res.status == 204
+
+ def list_nodes(self):
+ nodes = []
+ for vdc in self.vdcs:
+ res = self.connection.request(vdc)
+ elms = res.object.findall(fixxpath(
+ res.object, "ResourceEntities/ResourceEntity")
+ )
+ vapps = [
+ (i.get('name'), get_url_path(i.get('href')))
+ for i in elms
+ if i.get('type')
+ == 'application/vnd.vmware.vcloud.vApp+xml'
+ and i.get('name')
+ ]
+
+ for vapp_name, vapp_href in vapps:
+ res = self.connection.request(
+ vapp_href,
+ headers={
+ 'Content-Type':
+ 'application/vnd.vmware.vcloud.vApp+xml'
+ }
+ )
+ nodes.append(self._to_node(vapp_name, res.object))
+
+ return nodes
+
+ def _to_size(self, ram):
+ ns = NodeSize(
+ id=None,
+ name="%s Ram" % ram,
+ ram=ram,
+ disk=None,
+ bandwidth=None,
+ price=None,
+ driver=self.connection.driver
+ )
+ return ns
+
+ def list_sizes(self, location=None):
+ sizes = [self._to_size(i) for i in VIRTUAL_MEMORY_VALS]
+ return sizes
+
+ def _get_catalogitems_hrefs(self, catalog):
+ """Given a catalog href returns contained catalog item hrefs"""
+ res = self.connection.request(
+ catalog,
+ headers={
+ 'Content-Type':
+ 'application/vnd.vmware.vcloud.catalog+xml'
+ }
+ ).object
+
+ cat_items = res.findall(fixxpath(res, "CatalogItems/CatalogItem"))
+ cat_item_hrefs = [i.get('href')
+ for i in cat_items
+ if i.get('type') ==
+ 'application/vnd.vmware.vcloud.catalogItem+xml']
+
+ return cat_item_hrefs
+
+ def _get_catalogitem(self, catalog_item):
+ """Given a catalog item href returns elementree"""
+ res = self.connection.request(
+ catalog_item,
+ headers={
+ 'Content-Type':
+ 'application/vnd.vmware.vcloud.catalogItem+xml'
+ }
+ ).object
+
+ return res
+
+ def list_images(self, location=None):
+ images = []
+ for vdc in self.vdcs:
+ res = self.connection.request(vdc).object
+ res_ents = res.findall(fixxpath(
+ res, "ResourceEntities/ResourceEntity")
+ )
+ images += [
+ self._to_image(i)
+ for i in res_ents
+ if i.get('type') ==
+ 'application/vnd.vmware.vcloud.vAppTemplate+xml'
+ ]
+
+ for catalog in self._get_catalog_hrefs():
+ for cat_item in self._get_catalogitems_hrefs(catalog):
+ res = self._get_catalogitem(cat_item)
+ res_ents = res.findall(fixxpath(res, 'Entity'))
+ images += [
+ self._to_image(i)
+ for i in res_ents
+ if i.get('type') ==
+ 'application/vnd.vmware.vcloud.vAppTemplate+xml'
+ ]
+
+ return images
+
+ def create_node(self, **kwargs):
+ """Creates and returns node.
+
+ Non-standard optional keyword arguments:
+ network -- link to a "Network" e.g.,
+ "https://services.vcloudexpress.terremark.com/api/v0.8/network/7"
+ vdc -- link to a "VDC" e.g.,
+ "https://services.vcloudexpress.terremark.com/api/v0.8/vdc/1"
+ cpus -- number of virtual cpus (limit depends on provider)
+ password
+ row
+ group
+ """
+ name = kwargs['name']
+ image = kwargs['image']
+ size = kwargs['size']
+
+ # Some providers don't require a network link
+ try:
+ network = kwargs.get('network', self.networks[0].get('href'))
+ except IndexError:
+ network = ''
+
+ password = None
+ if kwargs.has_key('auth'):
+ auth = kwargs['auth']
+ if isinstance(auth, NodeAuthPassword):
+ password = auth.password
+ else:
+ raise ValueError('auth must be of NodeAuthPassword type')
+
+ instantiate_xml = InstantiateVAppXML(
+ name=name,
+ template=image.id,
+ net_href=network,
+ cpus=str(kwargs.get('cpus', 1)),
+ memory=str(size.ram),
+ password=password,
+ row=kwargs.get('row', None),
+ group=kwargs.get('group', None)
+ )
+
+ # Instantiate VM and get identifier.
+ res = self.connection.request(
+ '%s/action/instantiateVAppTemplate'
+ % kwargs.get('vdc', self.vdcs[0]),
+ data=instantiate_xml.tostring(),
+ method='POST',
+ headers={
+ 'Content-Type':
+ 'application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml'
+ }
+ )
+ vapp_name = res.object.get('name')
+ vapp_href = get_url_path(res.object.get('href'))
+
+ # Deploy the VM from the identifier.
+ res = self.connection.request('%s/action/deploy' % vapp_href,
+ method='POST')
+
+ self._wait_for_task_completion(res.object.get('href'))
+
+ # Power on the VM.
+ res = self.connection.request('%s/power/action/powerOn' % vapp_href,
+ method='POST')
+
+ res = self.connection.request(vapp_href)
+ node = self._to_node(vapp_name, res.object)
+
+ return node
+
+ features = {"create_node": ["password"]}
+
+class HostingComConnection(VCloudConnection):
+ host = "vcloud.safesecureweb.com"
+
+ def _get_auth_headers(self):
+ """hosting.com doesn't follow the standard vCloud authentication API"""
+ return {
+ 'Authentication':
+ base64.b64encode('%s:%s' % (self.user_id, self.key)),
+ 'Content-Length': 0
+ }
+
+class HostingComDriver(VCloudNodeDriver):
+ connectionCls = HostingComConnection
+
+class TerremarkConnection(VCloudConnection):
+ host = "services.vcloudexpress.terremark.com"
+
+class TerremarkDriver(VCloudNodeDriver):
+ connectionCls = TerremarkConnection
+ def list_locations(self):
+ return [NodeLocation(0, "Terremark Texas", 'US', self)]
diff --git a/libcloud/drivers/voxel.py b/libcloud/drivers/voxel.py
new file mode 100644
index 0000000..dcd70a1
--- /dev/null
+++ b/libcloud/drivers/voxel.py
@@ -0,0 +1,244 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Voxel VoxCloud driver
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+import datetime
+import hashlib
+from xml.etree import ElementTree as ET
+
+VOXEL_API_HOST = "api.voxel.net"
+
+class VoxelResponse(Response):
+
+ def __init__(self, response):
+ self.parsed = None
+ super(VoxelResponse, self).__init__(response)
+
+ def parse_body(self):
+ if not self.body:
+ return None
+ if not self.parsed:
+ self.parsed = ET.XML(self.body)
+ return self.parsed
+
+ def parse_error(self):
+ err_list = []
+ if not self.body:
+ return None
+ if not self.parsed:
+ self.parsed = ET.XML(self.body)
+ for err in self.parsed.findall('err'):
+ code = err.get('code')
+ err_list.append("(%s) %s" % (code, err.get('msg')))
+ # From voxel docs:
+ # 1: Invalid login or password
+ # 9: Permission denied: user lacks access rights for this method
+ if code == "1" or code == "9":
+ # sucks, but only way to detect
+ # bad authentication tokens so far
+ raise InvalidCredsException(err_list[-1])
+ return "\n".join(err_list)
+
+ def success(self):
+ if not self.parsed:
+ self.parsed = ET.XML(self.body)
+ stat = self.parsed.get('stat')
+ if stat != "ok":
+ return False
+ return True
+
+class VoxelConnection(ConnectionUserAndKey):
+
+ host = VOXEL_API_HOST
+ responseCls = VoxelResponse
+
+ def add_default_params(self, params):
+ params["key"] = self.user_id
+ params["timestamp"] = datetime.datetime.utcnow().isoformat()+"+0000"
+
+ for param in params.keys():
+ if params[param] is None:
+ del params[param]
+
+ keys = params.keys()
+ keys.sort()
+
+ md5 = hashlib.md5()
+ md5.update(self.key)
+ for key in keys:
+ if params[key]:
+ if not params[key] is None:
+ md5.update("%s%s"% (key, params[key]))
+ else:
+ md5.update(key)
+ params['api_sig'] = md5.hexdigest()
+ return params
+
+VOXEL_INSTANCE_TYPES = {}
+RAM_PER_CPU = 2048
+
+NODE_STATE_MAP = { 'IN_PROGRESS': NodeState.PENDING,
+ 'SUCCEEDED': NodeState.RUNNING,
+ 'shutting-down': NodeState.TERMINATED,
+ 'terminated': NodeState.TERMINATED }
+
+class VoxelNodeDriver(NodeDriver):
+
+ connectionCls = VoxelConnection
+ type = Provider.VOXEL
+ name = 'Voxel VoxCLOUD'
+
+ def initialize_instance_types():
+ for cpus in range(1,14):
+ if cpus == 1:
+ name = "Single CPU"
+ else:
+ name = "%d CPUs" % cpus
+ id = "%dcpu" % cpus
+ ram = cpus * RAM_PER_CPU
+
+ VOXEL_INSTANCE_TYPES[id]= {
+ 'id': id,
+ 'name': name,
+ 'ram': ram,
+ 'disk': None,
+ 'bandwidth': None,
+ 'price': None}
+
+ features = {"create_node": [],
+ "list_sizes": ["variable_disk"]}
+
+ initialize_instance_types()
+
+ def list_nodes(self):
+ params = {"method": "voxel.devices.list"}
+ result = self.connection.request('/', params=params).object
+ return self._to_nodes(result)
+
+ def list_sizes(self, location=None):
+ return [ NodeSize(driver=self.connection.driver, **i)
+ for i in VOXEL_INSTANCE_TYPES.values() ]
+
+ def list_images(self, location=None):
+ params = {"method": "voxel.images.list"}
+ result = self.connection.request('/', params=params).object
+ return self._to_images(result)
+
+ def create_node(self, **kwargs):
+ raise NotImplementedError, \
+ 'create_node not finished for voxel yet'
+ size = kwargs["size"]
+ cores = size.ram / RAM_PER_CPU
+ params = {'method': 'voxel.voxcloud.create',
+ 'hostname': kwargs["name"],
+ 'disk_size': int(kwargs["disk"])/1024,
+ 'processing_cores': cores,
+ 'facility': kwargs["location"].id,
+ 'image_id': kwargs["image"],
+ 'backend_ip': kwargs.get("privateip", None),
+ 'frontend_ip': kwargs.get("publicip", None),
+ 'admin_password': kwargs.get("rootpass", None),
+ 'console_password': kwargs.get("consolepass", None),
+ 'ssh_username': kwargs.get("sshuser", None),
+ 'ssh_password': kwargs.get("sshpass", None),
+ 'voxel_access': kwargs.get("voxel_access", None)}
+
+ object = self.connection.request('/', params=params).object
+
+ if self._getstatus(object):
+ return Node(
+ id = object.findtext("device/id"),
+ name = kwargs["name"],
+ state = NODE_STATE_MAP[object.findtext("devices/status")],
+ public_ip = public_ip,
+ private_ip = private_ip,
+ driver = self.connection.driver
+ )
+ else:
+ return None
+
+ def reboot_node(self, node):
+ """
+ Reboot the node by passing in the node object
+ """
+ params = {'method': 'voxel.devices.power',
+ 'device_id': node.id,
+ 'power_action': 'reboot'}
+ return self._getstatus(self.connection.request('/', params=params).object)
+
+ def destroy_node(self, node):
+ """
+ Destroy node by passing in the node object
+ """
+ params = {'method': 'voxel.voxcloud.delete',
+ 'device_id': node.id}
+ return self._getstatus(self.connection.request('/', params=params).object)
+
+ def list_locations(self):
+ params = {"method": "voxel.voxcloud.facilities.list"}
+ result = self.connection.request('/', params=params).object
+ nodes = self._to_locations(result)
+ return nodes
+
+ def _getstatus(self, element):
+ status = element.attrib["stat"]
+ return status == "ok"
+
+
+ def _to_locations(self, object):
+ return [NodeLocation(element.attrib["label"],
+ element.findtext("description"),
+ element.findtext("description"),
+ self)
+ for element in object.findall('facilities/facility')]
+
+ def _to_nodes(self, object):
+ nodes = []
+ for element in object.findall('devices/device'):
+ if element.findtext("type") == "Virtual Server":
+ try:
+ state = self.NODE_STATE_MAP[element.attrib['status']]
+ except KeyError:
+ state = NodeState.UNKNOWN
+
+ public_ip = private_ip = None
+ ipassignments = element.findall("ipassignments/ipassignment")
+ for ip in ipassignments:
+ if ip.attrib["type"] =="frontend":
+ public_ip = ip.text
+ elif ip.attrib["type"] == "backend":
+ private_ip = ip.text
+
+ nodes.append(Node(id= element.attrib['id'],
+ name=element.attrib['label'],
+ state=state,
+ public_ip= public_ip,
+ private_ip= private_ip,
+ driver=self.connection.driver))
+ return nodes
+
+ def _to_images(self, object):
+ images = []
+ for element in object.findall("images/image"):
+ images.append(NodeImage(id = element.attrib["id"],
+ name = element.attrib["summary"],
+ driver = self.connection.driver))
+ return images
diff --git a/libcloud/drivers/vpsnet.py b/libcloud/drivers/vpsnet.py
new file mode 100644
index 0000000..e7adb19
--- /dev/null
+++ b/libcloud/drivers/vpsnet.py
@@ -0,0 +1,181 @@
+# 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.
+# libcloud.org 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.
+"""
+VPS.net driver
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+
+import base64
+
+# 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
+
+API_HOST = 'api.vps.net'
+API_VERSION = 'api10json'
+
+RAM_PER_NODE = 256
+DISK_PER_NODE = 10
+BANDWIDTH_PER_NODE = 250
+PRICE_PER_NODE = {1: 20,
+ 2: 19,
+ 3: 18,
+ 4: 17,
+ 5: 16,
+ 6: 15,
+ 7: 14,
+ 15: 13,
+ 30: 12,
+ 60: 11,
+ 100: 10}
+
+class VPSNetResponse(Response):
+
+ def parse_body(self):
+ try:
+ js = json.loads(self.body)
+ return js
+ except ValueError:
+ return self.body
+
+ def success(self):
+ # vps.net wrongly uses 406 for invalid auth creds
+ if self.status == 406 or self.status == 403:
+ raise InvalidCredsException()
+ return True
+
+ def parse_error(self):
+ try:
+ errors = json.loads(self.body)['errors'][0]
+ except ValueError:
+ return self.body
+ else:
+ return "\n".join(errors)
+
+class VPSNetConnection(ConnectionUserAndKey):
+
+ host = API_HOST
+ responseCls = VPSNetResponse
+
+ def add_default_headers(self, headers):
+ user_b64 = base64.b64encode('%s:%s' % (self.user_id, self.key))
+ headers['Authorization'] = 'Basic %s' % (user_b64)
+ return headers
+
+class VPSNetNodeDriver(NodeDriver):
+
+ type = Provider.VPSNET
+ name = "vps.net"
+ connectionCls = VPSNetConnection
+
+ def _to_node(self, vm):
+ if vm['running']:
+ state = NodeState.RUNNING
+ else:
+ state = NodeState.PENDING
+
+ n = Node(id=vm['id'],
+ name=vm['label'],
+ state=state,
+ public_ip=[vm.get('primary_ip_address', None)],
+ private_ip=[],
+ driver=self.connection.driver)
+ return n
+
+ def _to_image(self, image, cloud):
+ image = NodeImage(id=image['id'],
+ name="%s: %s" % (cloud, image['label']),
+ driver=self.connection.driver)
+
+ return image
+
+ def _to_size(self, num):
+ size = NodeSize(id=num,
+ name="%d Node" % (num,),
+ ram=RAM_PER_NODE * num,
+ disk=DISK_PER_NODE,
+ bandwidth=BANDWIDTH_PER_NODE * num,
+ price=self._get_price_per_node(num) * num,
+ driver=self.connection.driver)
+ return size
+
+ def _get_price_per_node(self, num):
+ keys = sorted(PRICE_PER_NODE.keys())
+
+ if num >= max(keys):
+ return PRICE_PER_NODE[keys[-1]]
+
+ for i in range(0,len(keys)):
+ if keys[i] <= num < keys[i+1]:
+ return PRICE_PER_NODE[keys[i]]
+
+ def create_node(self, name, image, size, **kwargs):
+ headers = {'Content-Type': 'application/json'}
+ request = {'virtual_machine':
+ {'label': name,
+ 'fqdn': kwargs.get('fqdn', ''),
+ 'system_template_id': image.id,
+ 'backups_enabled': kwargs.get('backups_enabled', 0),
+ 'slices_required': size.id}}
+
+ res = self.connection.request('/virtual_machines.%s' % (API_VERSION,),
+ data=json.dumps(request),
+ headers=headers,
+ method='POST')
+ node = self._to_node(res.object['virtual_machine'])
+ return node
+
+ def reboot_node(self, node):
+ res = self.connection.request('/virtual_machines/%s/%s.%s' %
+ (node.id, 'reboot', API_VERSION),
+ method="POST")
+ node = self._to_node(res.object['virtual_machine'])
+ return True
+
+ def list_sizes(self, location=None):
+ res = self.connection.request('/nodes.%s' % (API_VERSION,))
+ available_nodes = len([size for size in res.object
+ if not size['slice']["virtual_machine_id"]])
+ sizes = [self._to_size(i) for i in range(1,available_nodes + 1)]
+ return sizes
+
+ def destroy_node(self, node):
+ res = self.connection.request('/virtual_machines/%s.%s'
+ % (node.id, API_VERSION),
+ method='DELETE')
+ return res.status == 200
+
+ def list_nodes(self):
+ res = self.connection.request('/virtual_machines.%s' % (API_VERSION,))
+ return [self._to_node(i['virtual_machine']) for i in res.object]
+
+ def list_images(self, location=None):
+ res = self.connection.request('/available_clouds.%s' % (API_VERSION,))
+
+ images = []
+ for cloud in res.object:
+ label = cloud['cloud']['label']
+ templates = cloud['cloud']['system_templates']
+ images.extend([self._to_image(image, label)
+ for image in templates])
+
+ return images
+
+ def list_locations(self):
+ return [NodeLocation(0, "VPS.net Western US", 'US', self)]
diff --git a/libcloud/interface.py b/libcloud/interface.py
new file mode 100644
index 0000000..6137564
--- /dev/null
+++ b/libcloud/interface.py
@@ -0,0 +1,328 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Provides zope.interface definitions for libcloud.
+"""
+from zope.interface import Interface, Attribute
+
+
+class INode(Interface):
+ """
+ A node (instance, etc)
+ """
+ uuid = Attribute("""Unique identifier""")
+ id = Attribute("""Unique ID provided by the provider (i-abcd1234, etc)""")
+ name = Attribute("""Hostname or similar identifier""")
+ state = Attribute("""A standard Node state as provided by L{NodeState}""")
+ public_ip = Attribute("""List of Public IPs of the Node""")
+ private_ip = Attribute("""List of Private IPs of the Node""")
+ driver = Attribute("""The NodeDriver that belongs to this Node""")
+ extra = Attribute("""Dict containing provider specific data""")
+
+ def get_uuid():
+ """
+ Provides a system wide unique ID for the node
+ """
+ def destroy():
+ """
+ Call `self.driver.destroy_node(self)`. A convenience method.
+ """
+
+ def reboot():
+ """
+ Call `self.driver.reboot_node(self)`. A convenience method.
+ """
+
+
+class INodeFactory(Interface):
+ """
+ Create nodes
+ """
+ def __call__(id, name, state, public_ip, private_ip, driver):
+ """
+ Set values for ivars, including any other requisite kwargs
+ """
+
+
+class INodeSize(Interface):
+ """
+ A machine image
+ """
+ id = Attribute("""Unique ID provided by the provider (m1.small, etc)""")
+ name = Attribute("""Name provided by the provider (Small CPU, etc)""")
+ ram = Attribute("""Amount of RAM provided in MB (256MB, 1740MB)""")
+ disk = Attribute("""Amount of disk provided in GB (200GB)""")
+ bandwidth = Attribute("""Amount of total transfer bandwidth in GB""")
+ price = Attribute("""Hourly price of this server in USD, estimated if
+ monthly""")
+ driver = Attribute("""The NodeDriver that belongs to this Image""")
+
+
+class INodeSizeFactory(Interface):
+ """
+ Create nodes
+ """
+ def __call__(id, name, ram, disk, bandwidth, price, driver):
+ """
+ Set values for ivars, including any other requisite kwargs
+ """
+
+
+class INodeImage(Interface):
+ """
+ A machine image
+ """
+ id = Attribute("""Unique ID provided by the provider (ami-abcd1234)""")
+ name = Attribute("""Name provided by the provider (Ubuntu 8.1)""")
+ driver = Attribute("""The NodeDriver that belongs to this Image""")
+ extra = Attribute("""Dict containing provider specific data""")
+
+class INodeImageFactory(Interface):
+ """
+ Create nodes
+ """
+ def __call__(id, name, driver):
+ """
+ Set values for ivars, including any other requisite kwargs
+ """
+
+class INodeLocation(Interface):
+ """
+ Physical Location of a node
+ """
+ id = Attribute("""Unique ID provided by the provider for a physical
+ datacenter""")
+ name = Attribute("""Name provided by the provider ('Austin Texas DC 1')""")
+ country = Attribute("""ISO 3166 country code of the physical location of
+ the data center <http://bit.ly/pKie5> (iso.org)""")
+ driver = Attribute("""The NodeDriver that belongs to this Location""")
+
+class INodeLocationFactory(Interface):
+ """
+ Create nodes location
+ """
+ def __call__(id, name, country, driver):
+ """
+ Set values for ivars, including any other requisite kwargs
+ """
+
+class INodeDriverFactory(Interface):
+ """
+ Create NodeDrivers
+ """
+ def __call__(key, secret=None, secure=True):
+ """
+ Set of value for ivars
+ """
+
+
+class INodeDriver(Interface):
+ """
+ A driver which provides nodes, such as an Amazon EC2 instance,
+ or Slicehost slice
+ """
+
+ connection = Attribute("""Represents the IConnection for this driver""")
+ type = Attribute("""The type of this provider as defined by L{Provider}""")
+ name = Attribute("""A pretty name (Linode, etc) for this provider""")
+
+ NODE_STATE_MAP = Attribute("""A mapping of states found in the response to
+ their standard type. This is a constant.""")
+
+ def create_node(**kwargs):
+ """
+ Creates a new node based on provided params. Name is ignored on
+ some providers.
+
+ To specify provider-specific options, use keyword arguments.
+ """
+
+ def destroy_node(node):
+ """
+ Returns True if the destroy was successful, otherwise False
+ """
+
+ def list_nodes():
+ """
+ Returns a list of nodes for this provider
+ """
+
+ def list_images(location=None):
+ """
+ Returns a list of images for this provider
+ """
+
+ def list_sizes(location=None):
+ """
+ Returns a list of sizes for this provider
+ """
+
+ def list_locations():
+ """
+ Returns a list of locations for this prodiver
+ """
+
+ def reboot_node(node):
+ """
+ Returns True if the reboot was successful, otherwise False
+ """
+
+class IConnection(Interface):
+ """
+ A Connection represents an interface between a Client and a Provider's Web
+ Service. It is capable of authenticating, making requests, and returning
+ responses.
+ """
+ conn_classes = Attribute("""Classes used to create connections, should be
+ in the form of `(insecure, secure)`""")
+ responseCls = Attribute("""Provider-specific Class used for creating
+ responses""")
+ connection = Attribute("""Represents the lower-level connection to the
+ server""")
+ host = Attribute("""Default host for this connection""")
+ port = Attribute("""Default port for this connection. This should be a
+ tuple of the form `(insecure, secure)` or for single-port
+ Providers, simply `(port,)`""")
+ secure = Attribute("""Indicates if this is a secure connection. If previous
+ recommendations were followed, it would be advantageous
+ for this to be in the form: 0=insecure, 1=secure""")
+ driver = Attribute("""The NodeDriver that belongs to this Node""")
+
+ def connect(host=None, port=None):
+ """
+ A method for establishing a connection. If no host or port are given,
+ existing ivars should be used.
+ """
+
+ def request(action, params={}, data='', method='GET'):
+ """
+ Make a request.
+
+ An `action` should represent a path, such as `/list/nodes`. Query
+ parameters necessary to the request should be passed in `params` and
+ any data to encode goes in `data`. `method` should be one of: (GET,
+ POST).
+
+ Should return a response object (specific to a provider).
+ """
+
+ def add_default_params(params):
+ """
+ Adds default parameters (such as API key, version, etc.)
+ to the passed `params`
+
+ Should return a dictionary.
+ """
+
+ def add_default_headers(headers):
+ """
+ Adds default headers (such as Authorization, X-Foo-Bar)
+ to the passed `headers`
+
+ Should return a dictionary.
+ """
+
+ def encode_data(data):
+ """
+ Data may need to be encoded before sent in a request.
+ If not, simply return the data.
+ """
+
+
+class IConnectionKey(IConnection):
+ """
+ IConnection which only depends on an API key for authentication.
+ """
+ key = Attribute("""API key, token, etc.""")
+
+
+class IConnectionUserAndKey(IConnectionKey):
+ """
+ IConnection which depends on a user identifier and an API
+ for authentication.
+ """
+ user_id = Attribute("""User identifier""")
+
+
+class IConnectionKeyFactory(Interface):
+ """
+ Create Connections which depend solely on an API key.
+ """
+ def __call__(key, secure=True):
+ """
+ Create a Connection.
+
+ The acceptance of only `key` provides support for APIs with only one
+ authentication bit.
+
+ The `secure` argument indicates whether or not a secure connection
+ should be made. Not all providers support this, so it may be ignored.
+ """
+
+
+class IConnectionUserAndKeyFactory(Interface):
+ """
+ Create Connections which depends on both a user identifier and API key.
+ """
+ def __call__(user_id, key, secure=True):
+ """
+ Create a Connection.
+
+ The first two arguments provide the initial values for `user_id` and
+ `key`, respectively, which should be used for authentication.
+
+ The `secure` argument indicates whether or not a secure connection
+ should be made. Not all providers support this, so it may be ignored.
+ """
+
+
+class IResponse(Interface):
+ """
+ A response as provided by a given HTTP Client.
+ """
+ object = Attribute("""The processed response object,
+ e.g. via lxml or json""")
+ body = Attribute("""Unparsed response body""")
+ status = Attribute("""Response status code""")
+ headers = Attribute("""Response headers""")
+ error = Attribute("""Response error, L{None} if no error.""")
+ connection = Attribute("""Represents the IConnection for this response""")
+
+ def parse_body():
+ """
+ Parse the response body (as XML, etc.)
+ """
+
+ def parse_error():
+ """
+ Parse the error that is contained in the response body (as XML, etc.)
+ """
+
+ def success():
+ """
+ Does the response indicate a successful request?
+ """
+
+
+class IResponseFactory(Interface):
+ """
+ Creates Responses.
+ """
+ def __call__(response):
+ """
+ Process the given response, setting ivars.
+ """
+
diff --git a/libcloud/providers.py b/libcloud/providers.py
new file mode 100644
index 0000000..b46d10f
--- /dev/null
+++ b/libcloud/providers.py
@@ -0,0 +1,54 @@
+# 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.
+# libcloud.org 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.
+"""
+Provider related utilities
+"""
+
+from libcloud.types import Provider
+
+DRIVERS = {
+ Provider.DUMMY:
+ ('libcloud.drivers.dummy', 'DummyNodeDriver'),
+ Provider.EC2_US_EAST:
+ ('libcloud.drivers.ec2', 'EC2NodeDriver'),
+ Provider.EC2_EU_WEST:
+ ('libcloud.drivers.ec2', 'EC2EUNodeDriver'),
+ Provider.EC2_US_WEST:
+ ('libcloud.drivers.ec2', 'EC2USWestNodeDriver'),
+ Provider.GOGRID:
+ ('libcloud.drivers.gogrid', 'GoGridNodeDriver'),
+ Provider.RACKSPACE:
+ ('libcloud.drivers.rackspace', 'RackspaceNodeDriver'),
+ Provider.SLICEHOST:
+ ('libcloud.drivers.slicehost', 'SlicehostNodeDriver'),
+ Provider.VPSNET:
+ ('libcloud.drivers.vpsnet', 'VPSNetNodeDriver'),
+ Provider.LINODE:
+ ('libcloud.drivers.linode', 'LinodeNodeDriver'),
+ Provider.RIMUHOSTING:
+ ('libcloud.drivers.rimuhosting', 'RimuHostingNodeDriver'),
+ Provider.VOXEL:
+ ('libcloud.drivers.voxel', 'VoxelNodeDriver'),
+}
+
+def get_driver(provider):
+ """Gets a driver
+ @param provider: Id of provider to get driver
+ @type provider: L{libcloud.types.Provider}
+ """
+ if provider in DRIVERS:
+ mod_name, driver_name = DRIVERS[provider]
+ _mod = __import__(mod_name, globals(), locals(), [driver_name])
+ return getattr(_mod, driver_name)
diff --git a/libcloud/types.py b/libcloud/types.py
new file mode 100644
index 0000000..adc9322
--- /dev/null
+++ b/libcloud/types.py
@@ -0,0 +1,71 @@
+# 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.
+# libcloud.org 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.
+"""
+Base types used by other parts of libcloud
+"""
+
+class Provider(object):
+ """
+ Defines for each of the supported providers
+
+ @cvar DUMMY: Example provider
+ @cvar EC2_US_EAST: Amazon AWS US N. Virgina
+ @cvar EC2_US_WEST: Amazon AWS US N. California
+ @cvar EC2_EU_WEST: Amazon AWS EU Ireland
+ @cvar RACKSPACE: Rackspace Cloud Servers
+ @cvar SLICEHOST: Slicehost.com
+ @cvar GOGRID: GoGrid
+ @cvar VPSNET: VPS.net
+ @cvar LINODE: Linode.com
+ @cvar VCLOUD: vmware vCloud
+ @cvar RIMUHOSTING: RimuHosting.com
+ """
+ DUMMY = 0
+ EC2 = 1 # deprecated name
+ EC2_US_EAST = 1
+ EC2_EU = 2 # deprecated name
+ EC2_EU_WEST = 2
+ RACKSPACE = 3
+ SLICEHOST = 4
+ GOGRID = 5
+ VPSNET = 6
+ LINODE = 7
+ VCLOUD = 8
+ RIMUHOSTING = 9
+ EC2_US_WEST = 10
+ VOXEL = 11
+
+class NodeState(object):
+ """
+ Standard states for a node
+
+ @cvar RUNNING: Node is running
+ @cvar REBOOTING: Node is rebooting
+ @cvar TERMINATED: Node is terminated
+ @cvar PENDING: Node is pending
+ @cvar UNKNOWN: Node state is unknown
+ """
+ RUNNING = 0
+ REBOOTING = 1
+ TERMINATED = 2
+ PENDING = 3
+ UNKNOWN = 4
+
+class InvalidCredsException(Exception):
+ """Exception used when invalid credentials are used on a provider."""
+ def __init__(self, value='Invalid credentials with the provider'):
+ self.value = value
+ def __str__(self):
+ return repr(self.value)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..6ebaa5c
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,100 @@
+# 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.
+# libcloud.org 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.
+import os
+import sys
+from distutils.core import setup
+from distutils.core import Command
+from unittest import TextTestRunner, TestLoader
+from glob import glob
+from os.path import splitext, basename, join as pjoin
+
+HTML_VIEWSOURCE_BASE = 'https://svn.apache.org/viewvc/incubator/libcloud/trunk'
+PROJECT_BASE_DIR = 'http://incubator.apache.org/libcloud/'
+
+class TestCommand(Command):
+ user_options = []
+
+ def initialize_options(self):
+ THIS_DIR = os.path.abspath(os.path.split(__file__)[0])
+ sys.path.insert(0, THIS_DIR)
+ sys.path.insert(0, pjoin(THIS_DIR, 'test'))
+ self._dir = os.getcwd()
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ testfiles = []
+ for t in glob(pjoin(self._dir, 'test', 'test_*.py')):
+ testfiles.append('.'.join(
+ ['test', splitext(basename(t))[0]])
+ )
+
+ tests = TestLoader().loadTestsFromNames(testfiles)
+ t = TextTestRunner(verbosity = 1)
+ res = t.run(tests)
+ sys.exit(not res.wasSuccessful())
+
+class ApiDocsCommand(Command):
+ user_options = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ os.system(
+ 'pydoctor'
+ ' --add-package=libcloud'
+ ' --project-name=libcloud'
+ ' --make-html'
+ ' --html-viewsource-base="%s"'
+ ' --project-base-dir=`pwd`'
+ ' --project-url="%s"'
+ % (HTML_VIEWSOURCE_BASE, PROJECT_BASE_DIR)
+ )
+
+setup(
+ name='apache-libcloud',
+ version='0.2.1',
+ description='A unified interface into many cloud server providers',
+ author='Apache Software Foundation',
+ author_email='libcloud@incubator.apache.org',
+ packages=[
+ 'libcloud',
+ 'libcloud.drivers'
+ ],
+ package_dir={
+ 'libcloud': 'libcloud',
+ 'libcloud.drivers': 'libcloud/drivers'
+ },
+ license='Apache License (2.0)',
+ url='http://incubator.apache.org/libcloud/',
+ cmdclass={
+ 'test': TestCommand,
+ 'apidocs': ApiDocsCommand
+ },
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Console',
+ 'Intended Audience :: System Administrators',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Software Development :: Libraries :: Python Modules'
+ ],
+)
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..04072ab
--- /dev/null
+++ b/test/__init__.py
@@ -0,0 +1,208 @@
+# 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.
+# libcloud.org 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.
+import httplib
+from cStringIO import StringIO
+from urllib2 import urlparse
+from cgi import parse_qs
+from libcloud.base import Node, NodeImage, NodeSize, NodeLocation
+from libcloud.types import NodeState
+import unittest
+
+class multipleresponse(object):
+ """
+ A decorator that allows MockHttp objects to return multi responses
+ """
+ count = 0
+ func = None
+
+ def __init__(self, f):
+ self.func = f
+
+ def __call__(self, *args, **kwargs):
+ ret = self.func(self.func.__class__, *args, **kwargs)
+ response = ret[self.count]
+ self.count = self.count + 1
+ return response
+
+
+class MockResponse(object):
+ """
+ A mock HTTPResponse
+ """
+ headers = {}
+ body = StringIO()
+ status = 0
+ reason = ''
+ version = 11
+
+ def __init__(self, status, body, headers=None, reason=None):
+ self.status = status
+ self.body = StringIO(body)
+ self.headers = headers or self.headers
+ self.reason = reason or self.reason
+
+ def read(self, *args, **kwargs):
+ return self.body.read(*args, **kwargs)
+
+ def getheader(self, name, *args, **kwargs):
+ return self.headers.get(name, *args, **kwargs)
+
+ def getheaders(self):
+ return self.headers.items()
+
+ def msg(self):
+ raise NotImplemented
+
+
+class MockHttp(object):
+ """
+ A mock HTTP client/server suitable for testing purposes. This replaces
+ `HTTPConnection` by implementing its API and returning a mock response.
+
+ Define methods by request path, replacing slashes (/) with underscores (_).
+ Each of these mock methods should return a tuple of:
+
+ (int status, str body, dict headers, str reason)
+
+ >>> mock = MockHttp('localhost', 8080)
+ >>> mock.request('GET', '/example/')
+ >>> response = mock.getresponse()
+ >>> response.body.read()
+ 'Hello World!'
+ >>> response.status
+ 200
+ >>> response.getheaders()
+ [('X-Foo', 'libcloud')]
+ >>> MockHttp.type = 'fail'
+ >>> mock.request('GET', '/example/')
+ >>> response = mock.getresponse()
+ >>> response.body.read()
+ 'Oh Noes!'
+ >>> response.status
+ 403
+ >>> response.getheaders()
+ [('X-Foo', 'fail')]
+
+ """
+ responseCls = MockResponse
+ host = None
+ port = None
+ response = None
+
+ type = None
+ use_param = None # will use this param to namespace the request function
+
+ def __init__(self, host, port, *args, **kwargs):
+ self.host = host
+ self.port = port
+
+ def request(self, method, url, body=None, headers=None):
+ # Find a method we can use for this request
+ parsed = urlparse.urlparse(url)
+ scheme, netloc, path, params, query, fragment = parsed
+ qs = parse_qs(query)
+ if path.endswith('/'):
+ path = path[:-1]
+ meth_name = path.replace('/','_').replace('.', '_').replace('-','_')
+ if self.type:
+ meth_name = '%s_%s' % (meth_name, self.type)
+ if self.use_param:
+ param = qs[self.use_param][0].replace('.', '_')
+ meth_name = '%s_%s' % (meth_name, param)
+ meth = getattr(self, meth_name)
+ status, body, headers, reason = meth(method, url, body, headers)
+ self.response = self.responseCls(status, body, headers, reason)
+
+ def getresponse(self):
+ return self.response
+
+ def connect(self):
+ """
+ Can't think of anything to mock here.
+ """
+ pass
+
+ def close(self):
+ pass
+
+ # Mock request/response example
+ def _example(self, method, url, body, headers):
+ """
+ Return a simple message and header, regardless of input.
+ """
+ return (httplib.OK, 'Hello World!', {'X-Foo': 'libcloud'},
+ httplib.responses[httplib.OK])
+
+ def _example_fail(self, method, url, body, headers):
+ return (httplib.FORBIDDEN, 'Oh Noes!', {'X-Foo': 'fail'},
+ httplib.responses[httplib.FORBIDDEN])
+
+
+class TestCaseMixin(object):
+
+ def test_list_nodes_response(self):
+ nodes = self.driver.list_nodes()
+ self.assertTrue(isinstance(nodes, list))
+ for node in nodes:
+ self.assertTrue(isinstance(node, Node))
+
+ def test_list_sizes_response(self):
+ sizes = self.driver.list_sizes()
+ size = sizes[0]
+ self.assertTrue(isinstance(sizes, list))
+ # Check that size values are ints or None
+ self.assertTrue(size.ram is None or isinstance(size.ram, int))
+ self.assertTrue(size.disk is None or isinstance(size.disk, int))
+ self.assertTrue(size.bandwidth is None or
+ isinstance(size.bandwidth, int))
+
+ def test_list_images_response(self):
+ images = self.driver.list_images()
+ self.assertTrue(isinstance(images, list))
+ for image in images:
+ self.assertTrue(isinstance(image, NodeImage))
+
+
+ def test_list_images_response(self):
+ locations = self.driver.list_locations()
+ self.assertTrue(isinstance(locations, list))
+ for dc in locations:
+ self.assertTrue(isinstance(dc, NodeLocation))
+
+ def test_create_node_response(self):
+ # should return a node object
+ size = self.driver.list_sizes()[0]
+ image = self.driver.list_images()[0]
+ node = self.driver.create_node(name='node-name',
+ image=image,
+ size=size)
+ self.assertTrue(isinstance(node, Node))
+
+ def test_destroy_node_response(self):
+ # should return a node object
+ node = self.driver.list_nodes()[0]
+ ret = self.driver.destroy_node(node)
+ self.assertTrue(isinstance(ret, bool))
+
+ def test_reboot_node_response(self):
+ # should return a node object
+ node = self.driver.list_nodes()[0]
+ ret = self.driver.reboot_node(node)
+ self.assertTrue(isinstance(ret, bool))
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
+
diff --git a/test/disabled_test_dummy.py b/test/disabled_test_dummy.py
new file mode 100644
index 0000000..3e9b150
--- /dev/null
+++ b/test/disabled_test_dummy.py
@@ -0,0 +1,34 @@
+# 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.
+# libcloud.org 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.
+import unittest
+
+from libcloud.providers import connect
+from libcloud.types import Provider, Node
+
+class DummyTests(unittest.TestCase):
+
+ def setUp(self):
+ self.conn = connect(Provider.DUMMY, 'foo')
+
+ def test_list_nodes(self):
+ ret = self.conn.list_nodes()
+
+ def test_reboot_node(self):
+ node = Node(None, None, None, None, None, attrs={})
+ ret = self.conn.reboot_node(node)
+
+ def test_create_node(self):
+ node = Node(None, None, None, None, None, attrs={})
+ ret = self.conn.create_node(node)
diff --git a/test/disabled_test_gogrid.py b/test/disabled_test_gogrid.py
new file mode 100644
index 0000000..96356d6
--- /dev/null
+++ b/test/disabled_test_gogrid.py
@@ -0,0 +1,28 @@
+# 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.
+# libcloud.org 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.
+import unittest
+
+from libcloud.providers import connect
+from libcloud.types import Provider
+
+from secrets import GOGRID_API_KEY, GOGRID_SECRET
+
+class GoGridTests(unittest.TestCase):
+
+ def setUp(self):
+ self.conn = connect(Provider.GOGRID, GOGRID_API_KEY, GOGRID_SECRET)
+
+ def test_list_nodes(self):
+ ret = self.conn.list_nodes()
diff --git a/test/disabled_test_vpsnet.py b/test/disabled_test_vpsnet.py
new file mode 100644
index 0000000..8a4aaf7
--- /dev/null
+++ b/test/disabled_test_vpsnet.py
@@ -0,0 +1,28 @@
+# 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.
+# libcloud.org 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.
+import unittest
+
+from libcloud.providers import connect
+from libcloud.types import Provider
+
+from secrets import VPSNET_USER, VPSNET_KEY
+
+class VPSNetTests(unittest.TestCase):
+
+ def setUp(self):
+ self.conn = connect(Provider.VPSNET, VPSNET_USER, VPSNET_KEY)
+
+ def test_list_nodes(self):
+ ret = self.conn.list_nodes()
diff --git a/test/secrets.py-dist b/test/secrets.py-dist
new file mode 100644
index 0000000..8f61adb
--- /dev/null
+++ b/test/secrets.py-dist
@@ -0,0 +1,43 @@
+# Licensed to libcloud.org under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# libcloud.org 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.
+
+# Make a copy of this file named 'secrets.py' and add your credentials there.
+# Note you can run unit tests without setting your credentials.
+
+# for test_ec2.py
+EC2_ACCESS_ID='YoUR K3Y'
+EC2_SECRET='secr3t'
+
+RACKSPACE_USER=''
+RACKSPACE_KEY=''
+
+SLICEHOST_KEY=''
+
+VPSNET_USER=''
+VPSNET_KEY=''
+
+GOGRID_API_KEY=''
+GOGRID_SECRET=''
+
+LINODE_KEY=''
+
+HOSTINGCOM_USER=''
+HOSTINGCOM_SECRET=''
+
+TERREMARK_USER=''
+TERREMARK_SECRET=''
+
+VOXEL_KEY=''
+VOXEL_SECRET=''
diff --git a/test/test_base.py b/test/test_base.py
new file mode 100644
index 0000000..d1cd63f
--- /dev/null
+++ b/test/test_base.py
@@ -0,0 +1,93 @@
+# 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.
+# libcloud.org 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.
+import unittest
+
+from libcloud.providers import DRIVERS, get_driver
+from libcloud.types import InvalidCredsException, Provider
+from libcloud.interface import INodeDriver
+from zope.interface.verify import verifyObject
+from zope.interface.exceptions import BrokenImplementation
+
+from libcloud.interface import IResponse, INode, INodeSize, INodeImage, INodeDriver
+from libcloud.interface import IConnectionKey, IConnectionUserAndKey
+from libcloud.base import Response, Node, NodeSize, NodeImage, NodeDriver
+from libcloud.base import ConnectionKey, ConnectionUserAndKey
+
+from test import MockResponse
+
+class FakeDriver(object):
+ type = 0
+
+class BaseTests(unittest.TestCase):
+
+ def test_base_node(self):
+ node = Node(id=0, name=0, state=0, public_ip=0, private_ip=0,
+ driver=FakeDriver())
+ verifyObject(INode, node)
+
+ def test_base_node_size(self):
+ node_size = NodeSize(id=0, name=0, ram=0, disk=0, bandwidth=0, price=0,
+ driver=FakeDriver())
+ verifyObject(INodeSize, node_size)
+
+ def test_base_node_image(self):
+ node_image = NodeImage(id=0, name=0, driver=FakeDriver())
+ verifyObject(INodeImage, node_image)
+
+ def test_base_response(self):
+ verifyObject(IResponse, Response(MockResponse(status=200,
+ body='foo')))
+
+ def test_base_node_driver(self):
+ node_driver = NodeDriver('foo')
+ verifyObject(INodeDriver, node_driver)
+
+ def test_base_connection_key(self):
+ conn = ConnectionKey('foo')
+ verifyObject(IConnectionKey, conn)
+
+ def test_base_connection_userkey(self):
+ conn = ConnectionUserAndKey('foo', 'bar')
+ verifyObject(IConnectionUserAndKey, conn)
+
+# def test_drivers_interface(self):
+# failures = []
+# for driver in DRIVERS:
+# creds = ProviderCreds(driver, 'foo', 'bar')
+# try:
+# verifyObject(INodeDriver, get_driver(driver)(creds))
+# except BrokenImplementation:
+# failures.append(DRIVERS[driver][1])
+#
+# if failures:
+# self.fail('the following drivers do not support the \
+# INodeDriver interface: %s' % (', '.join(failures)))
+
+# def test_invalid_creds(self):
+# failures = []
+# for driver in DRIVERS:
+# if driver == Provider.DUMMY:
+# continue
+# conn = connect(driver, 'bad', 'keys')
+# try:
+# conn.list_nodes()
+# except InvalidCredsException:
+# pass
+# else:
+# failures.append(DRIVERS[driver][1])
+#
+# if failures:
+# self.fail('the following drivers did not throw an \
+# InvalidCredsException: %s' % (', '.join(failures)))
diff --git a/test/test_ec2.py b/test/test_ec2.py
new file mode 100644
index 0000000..e09bf91
--- /dev/null
+++ b/test/test_ec2.py
@@ -0,0 +1,187 @@
+# 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.
+# libcloud.org 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.
+import unittest
+
+from libcloud.drivers.ec2 import EC2NodeDriver
+from libcloud.base import Node, NodeImage, NodeSize
+
+from test import MockHttp, TestCaseMixin
+
+import httplib
+
+from secrets import EC2_ACCESS_ID, EC2_SECRET
+
+class EC2Tests(unittest.TestCase, TestCaseMixin):
+
+ def setUp(self):
+ EC2NodeDriver.connectionCls.conn_classes = (None, EC2MockHttp)
+ EC2MockHttp.use_param = 'Action'
+ self.driver = EC2NodeDriver(EC2_ACCESS_ID, EC2_SECRET)
+
+ def test_create_node(self):
+ image = NodeImage(id='ami-be3adfd7',
+ name='ec2-public-images/fedora-8-i386-base-v1.04.manifest.xml',
+ driver=self.driver)
+ size = NodeSize('m1.small', 'Small Instance', None, None, None, None, driver=self.driver)
+ node = self.driver.create_node(name='foo', image=image, size=size)
+ self.assertEqual(node.id, 'i-2ba64342')
+
+ def test_list_nodes(self):
+ node = self.driver.list_nodes()[0]
+ self.assertEqual(node.id, 'i-4382922a')
+
+ def test_reboot_node(self):
+ node = Node('i-4382922a', None, None, None, None, self.driver)
+ ret = self.driver.reboot_node(node)
+ self.assertTrue(ret)
+
+ def test_destroy_node(self):
+ node = Node('i-4382922a', None, None, None, None, self.driver)
+ ret = self.driver.destroy_node(node)
+ self.assertTrue(ret)
+
+ def test_list_sizes(self):
+ sizes = self.driver.list_sizes()
+ self.assertEqual(len(sizes), 7)
+ self.assertTrue('m1.small' in [ s.id for s in sizes])
+ self.assertTrue('m1.large' in [ s.id for s in sizes])
+ self.assertTrue('m1.xlarge' in [ s.id for s in sizes])
+ self.assertTrue('m2.2xlarge' in [ s.id for s in sizes])
+
+ def test_list_images(self):
+ images = self.driver.list_images()
+ image = images[0]
+ self.assertEqual(len(images), 1)
+ self.assertEqual(image.name, 'ec2-public-images/fedora-8-i386-base-v1.04.manifest.xml')
+ self.assertEqual(image.id, 'ami-be3adfd7')
+
+class EC2MockHttp(MockHttp):
+ def _DescribeInstances(self, method, url, body, headers):
+ body = """<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+ <requestId>56d0fffa-8819-4658-bdd7-548f143a86d2</requestId>
+ <reservationSet>
+ <item>
+ <reservationId>r-07adf66e</reservationId>
+ <ownerId>822272953071</ownerId>
+ <groupSet>
+ <item>
+ <groupId>default</groupId>
+ </item>
+ </groupSet>
+ <instancesSet>
+ <item>
+ <instanceId>i-4382922a</instanceId>
+ <imageId>ami-0d57b264</imageId>
+ <instanceState>
+ <code>0</code>
+ <name>pending</name>
+ </instanceState>
+ <privateDnsName/>
+ <dnsName/>
+ <reason/>
+ <amiLaunchIndex>0</amiLaunchIndex>
+ <productCodes/>
+ <instanceType>m1.small</instanceType>
+ <launchTime>2009-08-07T05:47:04.000Z</launchTime>
+ <placement>
+ <availabilityZone>us-east-1a</availabilityZone>
+ </placement>
+ <monitoring>
+ <state>disabled</state>
+ </monitoring>
+ </item>
+ </instancesSet>
+ </item>
+ </reservationSet>
+</DescribeInstancesResponse>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _RebootInstances(self, method, url, body, headers):
+ body = """<RebootInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+ <requestId>76dabb7a-fb39-4ed1-b5e0-31a4a0fdf5c0</requestId>
+ <return>true</return>
+</RebootInstancesResponse>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _DescribeImages(self, method, url, body, headers):
+ body = """<DescribeImagesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+ <imagesSet>
+ <item>
+ <imageId>ami-be3adfd7</imageId>
+ <imageLocation>ec2-public-images/fedora-8-i386-base-v1.04.manifest.xml</imageLocation>
+ <imageState>available</imageState>
+ <imageOwnerId>206029621532</imageOwnerId>
+ <isPublic>false</isPublic>
+ <architecture>i386</architecture>
+ <imageType>machine</imageType>
+ <kernelId>aki-4438dd2d</kernelId>
+ <ramdiskId>ari-4538dd2c</ramdiskId>
+ </item>
+ </imagesSet>
+ </DescribeImagesResponse>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _RunInstances(self, method, url, body, headers):
+ body = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+ <reservationId>r-47a5402e</reservationId>
+ <ownerId>AIDADH4IGTRXXKCD</ownerId>
+ <groupSet>
+ <item>
+ <groupId>default</groupId>
+ </item>
+ </groupSet>
+ <instancesSet>
+ <item>
+ <instanceId>i-2ba64342</instanceId>
+ <imageId>ami-be3adfd7</imageId>
+ <instanceState>
+ <code>0</code>
+ <name>pending</name>
+ </instanceState>
+ <privateDnsName></privateDnsName>
+ <dnsName></dnsName>
+ <keyName>example-key-name</keyName>
+ <amiLaunchIndex>0</amiLaunchIndex>
+ <instanceType>m1.small</instanceType>
+ <launchTime>2007-08-07T11:51:50.000Z</launchTime>
+ <placement>
+ <availabilityZone>us-east-1b</availabilityZone>
+ </placement>
+ <monitoring>
+ <enabled>true</enabled>
+ </monitoring>
+ </item>
+ </instancesSet>
+ </RunInstancesResponse>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _TerminateInstances(self, method, url, body, headers):
+ body = """<TerminateInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+ <requestId>fa63083d-e0f7-4933-b31a-f266643bdee8</requestId>
+ <instancesSet>
+ <item>
+ <instanceId>i-4382922a</instanceId>
+ <shutdownState>
+ <code>32</code>
+ <name>shutting-down</name>
+ </shutdownState>
+ <previousState>
+ <code>16</code>
+ <name>running</name>
+ </previousState>
+ </item>
+ </instancesSet>
+</TerminateInstancesResponse>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
diff --git a/test/test_linode.py b/test/test_linode.py
new file mode 100644
index 0000000..2ee76cd
--- /dev/null
+++ b/test/test_linode.py
@@ -0,0 +1,133 @@
+# 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.
+# libcloud.org 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 <jsmith@linode.com>
+# Based upon code written by Alex Polvi <polvi@cloudkick.com>
+#
+
+from libcloud.drivers.linode import LinodeNodeDriver
+from libcloud.base import Node, NodeAuthPassword
+from test import MockHttp, TestCaseMixin
+
+import unittest
+import httplib
+
+class LinodeTest(unittest.TestCase, TestCaseMixin):
+ # The Linode test suite
+
+ def setUp(self):
+ LinodeNodeDriver.connectionCls.conn_classes = (None, LinodeMockHttp)
+ LinodeMockHttp.use_param = 'api_action'
+ self.driver = LinodeNodeDriver('foo')
+
+ 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.assertTrue('75.127.96.245' in node.public_ip)
+ self.assertEqual(node.private_ip, [])
+
+ 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(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=NodeAuthPassword("test123"))
+
+ def test_list_sizes(self):
+ sizes = self.driver.list_sizes()
+ self.assertEqual(len(sizes), 10)
+ 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), 22)
+
+ 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))
+
+
+class LinodeMockHttp(MockHttp):
+ def _avail_datacenters(self, method, url, body, headers):
+ body = '{"ERRORARRAY":[],"ACTION":"avail.datacenters","DATA":[{"DATACENTERID":2,"LOCATION":"Dallas, TX, USA"},{"DATACENTERID":3,"LOCATION":"Fremont, CA, USA"},{"DATACENTERID":4,"LOCATION":"Atlanta, GA, USA"},{"DATACENTERID":6,"LOCATION":"Newark, NJ, USA"}]}'
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _avail_linodeplans(self, method, url, body, headers):
+ body = '{"ERRORARRAY":[],"ACTION":"avail.linodeplans","DATA":[{"AVAIL":{"2":27,"3":0,"4":0,"6":0},"DISK":16,"PRICE":19.95,"PLANID":1,"LABEL":"Linode 360","RAM":360,"XFER":200},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":24,"PRICE":29.95,"PLANID":2,"LABEL":"Linode 540","RAM":540,"XFER":300},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":32,"PRICE":39.95,"PLANID":3,"LABEL":"Linode 720","RAM":720,"XFER":400},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":48,"PRICE":59.95,"PLANID":4,"LABEL":"Linode 1080","RAM":1080,"XFER":600},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":64,"PRICE":79.95,"PLANID":5,"LABEL":"Linode 1440","RAM":1440,"XFER":800},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":128,"PRICE":159.95,"PLANID":6,"LABEL":"Linode 2880","RAM":2880,"XFER":1600},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":256,"PRICE":319.95,"PLANID":7,"LABEL":"Linode 5760","RAM":5760,"XFER":2000},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":384,"PRICE":479.95,"PLANID":8,"LABEL":"Linode 8640","RAM":8640,"XFER":2000},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":512,"PRICE":639.95,"PLANID":9,"LABEL":"Linode 11520","RAM":11520,"XFER":2000},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":640,"PRICE":799.95,"PLANID":10,"LABEL":"Linode 14400","RAM":14400,"XFER":2000}]}'
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _avail_distributions(self, method, url, body, headers):
+ body = '{"ERRORARRAY":[],"ACTION":"avail.distributions","DATA":[{"IS64BIT":0,"LABEL":"Arch Linux 2007.08","MINIMAGESIZE":436,"DISTRIBUTIONID":38,"CREATE_DT":"2007-10-24 00:00:00.0"},{"IS64BIT":0,"LABEL":"Centos 5.0","MINIMAGESIZE":594,"DISTRIBUTIONID":32,"CREATE_DT":"2007-04-27 00:00:00.0"},{"IS64BIT":0,"LABEL":"Centos 5.2","MINIMAGESIZE":950,"DISTRIBUTIONID":46,"CREATE_DT":"2008-11-30 00:00:00.0"},{"IS64BIT":1,"LABEL":"Centos 5.2 64bit","MINIMAGESIZE":980,"DISTRIBUTIONID":47,"CREATE_DT":"2008-11-30 00:00:00.0"},{"IS64BIT":0,"LABEL":"Debian 4.0","MINIMAGESIZE":200,"DISTRIBUTIONID":28,"CREATE_DT":"2007-04-18 00:00:00.0"},{"IS64BIT":1,"LABEL":"Debian 4.0 64bit","MINIMAGESIZE":220,"DISTRIBUTIONID":48,"CREATE_DT":"2008-12-02 00:00:00.0"},{"IS64BIT":0,"LABEL":"Debian 5.0","MINIMAGESIZE":200,"DISTRIBUTIONID":50,"CREATE_DT":"2009-02-19 00:00:00.0"},{"IS64BIT":1,"LABEL":"Debian 5.0 64bit","MINIMAGESIZE":300,"DISTRIBUTIONID":51,"CREATE_DT":"2009-02-19 00:00:00.0"},{"IS64BIT":0,"LABEL":"Fedora 8","MINIMAGESIZE":740,"DISTRIBUTIONID":40,"CREATE_DT":"2007-11-09 00:00:00.0"},{"IS64BIT":0,"LABEL":"Fedora 9","MINIMAGESIZE":1175,"DISTRIBUTIONID":43,"CREATE_DT":"2008-06-09 15:15:21.0"},{"IS64BIT":0,"LABEL":"Gentoo 2007.0","MINIMAGESIZE":1800,"DISTRIBUTIONID":35,"CREATE_DT":"2007-08-29 00:00:00.0"},{"IS64BIT":0,"LABEL":"Gentoo 2008.0","MINIMAGESIZE":1500,"DISTRIBUTIONID":52,"CREATE_DT":"2009-03-20 00:00:00.0"},{"IS64BIT":1,"LABEL":"Gentoo 2008.0 64bit","MINIMAGESIZE":2500,"DISTRIBUTIONID":53,"CREATE_DT":"2009-04-04 00:00:00.0"},{"IS64BIT":0,"LABEL":"OpenSUSE 11.0","MINIMAGESIZE":850,"DISTRIBUTIONID":44,"CREATE_DT":"2008-08-21 08:32:16.0"},{"IS64BIT":0,"LABEL":"Slackware 12.0","MINIMAGESIZE":315,"DISTRIBUTIONID":34,"CREATE_DT":"2007-07-16 00:00:00.0"},{"IS64BIT":0,"LABEL":"Slackware 12.2","MINIMAGESIZE":500,"DISTRIBUTIONID":54,"CREATE_DT":"2009-04-04 00:00:00.0"},{"IS64BIT":0,"LABEL":"Ubuntu 8.04 LTS","MINIMAGESIZE":400,"DISTRIBUTIONID":41,"CREATE_DT":"2008-04-23 15:11:29.0"},{"IS64BIT":1,"LABEL":"Ubuntu 8.04 LTS 64bit","MINIMAGESIZE":350,"DISTRIBUTIONID":42,"CREATE_DT":"2008-06-03 12:51:11.0"},{"IS64BIT":0,"LABEL":"Ubuntu 8.10","MINIMAGESIZE":220,"DISTRIBUTIONID":45,"CREATE_DT":"2008-10-30 23:23:03.0"},{"IS64BIT":1,"LABEL":"Ubuntu 8.10 64bit","MINIMAGESIZE":230,"DISTRIBUTIONID":49,"CREATE_DT":"2008-12-02 00:00:00.0"},{"IS64BIT":0,"LABEL":"Ubuntu 9.04","MINIMAGESIZE":350,"DISTRIBUTIONID":55,"CREATE_DT":"2009-04-23 00:00:00.0"},{"IS64BIT":1,"LABEL":"Ubuntu 9.04 64bit","MINIMAGESIZE":350,"DISTRIBUTIONID":56,"CREATE_DT":"2009-04-23 00:00:00.0"}]}'
+ 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_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_delete(self, method, url, body, headers):
+ body = '{"ERRORARRAY":[],"ACTION":"linode.delete","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 = '{"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}]}'
+ 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":1299,"DiskID":55648}}'
+ 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 = '{"ACTION": "linode.list", "DATA": [{"ALERT_DISKIO_ENABLED": 1, "BACKUPWEEKLYDAY": 0, "LABEL": "api-node3", "DATACENTERID": 5, "ALERT_BWOUT_ENABLED": 1, "ALERT_CPU_THRESHOLD": 10, "TOTALHD": 100, "ALERT_BWQUOTA_THRESHOLD": 81, "ALERT_BWQUOTA_ENABLED": 1, "TOTALXFER": 200, "STATUS": 2, "ALERT_BWIN_ENABLED": 1, "ALERT_BWIN_THRESHOLD": 5, "ALERT_DISKIO_THRESHOLD": 200, "WATCHDOG": 1, "LINODEID": 8098, "BACKUPWINDOW": 1, "TOTALRAM": 540, "LPM_DISPLAYGROUP": "", "ALERT_BWOUT_THRESHOLD": 5, "BACKUPSENABLED": 1, "ALERT_CPU_ENABLED": 1}], "ERRORARRAY": []}'
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _linode_ip_list(self, method, url, body, headers):
+ body = '{"ACTION": "linode.ip.list", "DATA": [{"RDNS_NAME": "li22-54.members.linode.com", "ISPUBLIC": 1, "IPADDRESS": "75.127.96.54", "IPADDRESSID": 5384, "LINODEID": 8098}, {"RDNS_NAME": "li22-245.members.linode.com", "ISPUBLIC": 1, "IPADDRESS": "75.127.96.245", "IPADDRESSID": 5575, "LINODEID": 8098}], "ERRORARRAY": []}'
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
diff --git a/test/test_rackspace.py b/test/test_rackspace.py
new file mode 100644
index 0000000..9fca8a1
--- /dev/null
+++ b/test/test_rackspace.py
@@ -0,0 +1,144 @@
+# 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.
+# libcloud.org 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.
+import unittest
+
+from libcloud.types import InvalidCredsException
+from libcloud.drivers.rackspace import RackspaceNodeDriver as Rackspace
+from libcloud.base import Node, NodeImage, NodeSize
+
+from test import MockHttp, TestCaseMixin
+from secrets import RACKSPACE_USER, RACKSPACE_KEY
+import httplib
+
+class RackspaceTests(unittest.TestCase, TestCaseMixin):
+
+ def setUp(self):
+ Rackspace.connectionCls.conn_classes = (None, RackspaceMockHttp)
+ RackspaceMockHttp.type = None
+ self.driver = Rackspace(RACKSPACE_USER, RACKSPACE_KEY)
+
+ def test_auth(self):
+ RackspaceMockHttp.type = 'UNAUTHORIZED'
+ try:
+ self.driver = Rackspace(RACKSPACE_USER, RACKSPACE_KEY)
+ except InvalidCredsException, e:
+ self.assertEqual(True, isinstance(e, InvalidCredsException))
+ else:
+ self.fail('test should have thrown')
+
+ def test_list_nodes(self):
+ RackspaceMockHttp.type = 'EMPTY'
+ ret = self.driver.list_nodes()
+ self.assertEqual(len(ret), 0)
+ RackspaceMockHttp.type = None
+ ret = self.driver.list_nodes()
+ self.assertEqual(len(ret), 1)
+ node = ret[0]
+ self.assertEqual('67.23.21.33', node.public_ip[0])
+ self.assertEqual('10.176.168.218', node.private_ip[0])
+ self.assertEqual(node.extra.get('flavorId'), '1')
+ self.assertEqual(node.extra.get('imageId'), '11')
+
+ def test_list_sizes(self):
+ ret = self.driver.list_sizes()
+ self.assertEqual(len(ret), 7)
+ size = ret[0]
+ self.assertEqual(size.name, '256 slice')
+
+ def test_list_images(self):
+ ret = self.driver.list_images()
+ self.assertEqual(ret[10].extra['serverId'], None)
+ self.assertEqual(ret[11].extra['serverId'], '91221')
+
+ def test_create_node(self):
+ image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)', driver=self.driver)
+ size = NodeSize(1, '256 slice', None, None, None, None, driver=self.driver)
+ node = self.driver.create_node(name='racktest', image=image, size=size)
+ self.assertEqual(node.name, 'racktest')
+ self.assertEqual(node.extra.get('password'), 'racktestvJq7d3')
+
+ def test_create_node_with_metadata(self):
+ RackspaceMockHttp.type = 'METADATA'
+ image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)', driver=self.driver)
+ size = NodeSize(1, '256 slice', None, None, None, None, driver=self.driver)
+ metadata = { 'a': 'b', 'c': 'd' }
+ files = { '/file1': 'content1', '/file2': 'content2' }
+ node = self.driver.create_node(name='racktest', image=image, size=size, metadata=metadata, files=files)
+ self.assertEqual(node.name, 'racktest')
+ self.assertEqual(node.extra.get('password'), 'racktestvJq7d3')
+ self.assertEqual(node.extra.get('metadata'), metadata)
+
+ def test_reboot_node(self):
+ node = Node(id=72258, name=None, state=None, public_ip=None, private_ip=None,
+ driver=self.driver)
+ ret = node.reboot()
+ self.assertTrue(ret is True)
+
+ def test_destroy_node(self):
+ node = Node(id=72258, name=None, state=None, public_ip=None, private_ip=None,
+ driver=self.driver)
+ ret = node.destroy()
+ self.assertTrue(ret is True)
+
+class RackspaceMockHttp(MockHttp):
+
+ # fake auth token response
+ def _v1_0(self, method, url, body, headers):
+ headers = {'x-server-management-url': 'https://servers.api.rackspacecloud.com/v1.0/slug',
+ 'x-auth-token': 'FE011C19-CF86-4F87-BE5D-9229145D7A06',
+ 'x-cdn-management-url': 'https://cdn.clouddrive.com/v1/MossoCloudFS_FE011C19-CF86-4F87-BE5D-9229145D7A06',
+ 'x-storage-token': 'FE011C19-CF86-4F87-BE5D-9229145D7A06',
+ 'x-storage-url': 'https://storage4.clouddrive.com/v1/MossoCloudFS_FE011C19-CF86-4F87-BE5D-9229145D7A06'}
+
+ return (httplib.NO_CONTENT, "", headers, httplib.responses[httplib.NO_CONTENT])
+ def _v1_0_UNAUTHORIZED(self, method, url, body, headers):
+ return (httplib.UNAUTHORIZED, "", {}, httplib.responses[httplib.UNAUTHORIZED])
+
+ def _v1_0_slug_servers_detail_EMPTY(self, method, url, body, headers):
+ body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><servers xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _v1_0_slug_servers_detail(self, method, url, body, headers):
+ body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><servers xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"><server status="ACTIVE" progress="100" hostId="9dd380940fcbe39cb30255ed4664f1f3" flavorId="1" imageId="11" id="72258" name="racktest"><metadata/><addresses><public><ip addr="67.23.21.33"/></public><private><ip addr="10.176.168.218"/></private></addresses></server></servers>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _v1_0_slug_flavors_detail(self, method, url, body, headers):
+ body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><flavors xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"><flavor disk="10" ram="256" name="256 slice" id="1"/><flavor disk="20" ram="512" name="512 slice" id="2"/><flavor disk="40" ram="1024" name="1GB slice" id="3"/><flavor disk="80" ram="2048" name="2GB slice" id="4"/><flavor disk="160" ram="4096" name="4GB slice" id="5"/><flavor disk="320" ram="8192" name="8GB slice" id="6"/><flavor disk="620" ram="15872" name="15.5GB slice" id="7"/></flavors>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _v1_0_slug_images_detail(self, method, url, body, headers):
+ body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><images xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="CentOS 5.2" id="2"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Gentoo 2008.0" id="3"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Debian 5.0 (lenny)" id="4"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Fedora 10 (Cambridge)" id="5"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="CentOS 5.3" id="7"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Ubuntu 9.04 (jaunty)" id="8"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Arch 2009.02" id="9"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Ubuntu 8.04.2 LTS (hardy)" id="10"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Ubuntu 8.10 (intrepid)" id="11"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Red Hat EL 5.3" id="12"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Fedora 11 (Leonidas)" id="13"/><image status="ACTIVE" progress="100" created="2009-11-29T20:22:09-06:00" updated="2009-11-29T20:24:08-06:00" serverId="91221" name="daily" id="191234"/></images>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _v1_0_slug_servers(self, method, url, body, headers):
+ body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" status="BUILD" progress="0" hostId="9dd380940fcbe39cb30255ed4664f1f3" flavorId="1" imageId="11" adminPass="racktestvJq7d3" id="72258" name="racktest"><metadata/><addresses><public><ip addr="67.23.21.33"/></public><private><ip addr="10.176.168.218"/></private></addresses></server>"""
+ return (httplib.ACCEPTED, body, {}, httplib.responses[httplib.ACCEPTED])
+
+ def _v1_0_slug_servers_METADATA(self, method, url, body, headers):
+ body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" status="BUILD" progress="0" hostId="9dd380940fcbe39cb30255ed4664f1f3" flavorId="1" imageId="11" adminPass="racktestvJq7d3" id="72258" name="racktest"><metadata><meta key="a">b</meta><meta key="c">d</meta></metadata><addresses><public><ip addr="67.23.21.33"/></public><private><ip addr="10.176.168.218"/></private></addresses></server>"""
+ return (httplib.ACCEPTED, body, {}, httplib.responses[httplib.ACCEPTED])
+
+ def _v1_0_slug_servers_72258_action(self, method, url, body, headers):
+ if method != "POST" or body[:8] != "<reboot ":
+ raise NotImplemented
+ # only used by reboot() right now, but we will need to parse body someday !!!!
+ return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED])
+
+ def _v1_0_slug_servers_72258(self, method, url, body, headers):
+ if method != "DELETE":
+ raise NotImplemented
+ # only used by destroy node()
+ return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED])
+
diff --git a/test/test_rimuhosting.py b/test/test_rimuhosting.py
new file mode 100644
index 0000000..13f954a
--- /dev/null
+++ b/test/test_rimuhosting.py
@@ -0,0 +1,285 @@
+# 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.
+# libcloud.org 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.
+# Copyright 2009 RedRata Ltd
+
+from libcloud.drivers.rimuhosting import RimuHostingNodeDriver
+from test import MockHttp
+from test import MockHttp, TestCaseMixin
+
+import unittest
+import httplib
+
+class RimuHostingTest(unittest.TestCase, TestCaseMixin):
+ def setUp(self):
+ RimuHostingNodeDriver.connectionCls.conn_classes = (None,
+ RimuHostingMockHttp)
+ self.driver = RimuHostingNodeDriver('foo')
+
+ def test_list_nodes(self):
+ nodes = self.driver.list_nodes()
+ self.assertEqual(len(nodes),1)
+ node = nodes[0]
+ self.assertEqual(node.public_ip[0], "1.2.3.4")
+ self.assertEqual(node.public_ip[1], "1.2.3.5")
+ self.assertEqual(node.extra['order_oid'], 88833465)
+ self.assertEqual(node.id, "order-88833465-api-ivan-net-nz")
+
+ def test_list_sizes(self):
+ sizes = self.driver.list_sizes()
+ self.assertEqual(len(sizes),1)
+ size = sizes[0]
+ self.assertEqual(size.ram,950)
+ self.assertEqual(size.disk,20)
+ self.assertEqual(size.bandwidth,75)
+ self.assertEqual(size.price,32.54)
+
+ def test_list_images(self):
+ images = self.driver.list_images()
+ self.assertEqual(len(images),6)
+ image = images[0]
+ self.assertEqual(image.name,"Debian 5.0 (aka Lenny, RimuHosting"\
+ " recommended distro)")
+ self.assertEqual(image.id, "lenny")
+
+ def test_reboot_node(self):
+ # Raises exception on failure
+ node = self.driver.list_nodes()[0]
+ self.driver.reboot_node(node)
+
+ def test_destroy_node(self):
+ # Raises exception on failure
+ node = self.driver.list_nodes()[0]
+ self.driver.destroy_node(node)
+
+ def test_create_node(self):
+ # Raises exception on failure
+ size = self.driver.list_sizes()[0]
+ image = self.driver.list_images()[0]
+ self.driver.create_node(name="api.ivan.net.nz", image=image, size=size)
+
+class RimuHostingMockHttp(MockHttp):
+ def _r_orders(self,method,url,body,headers):
+ body = """
+ { "get_orders_response" :
+ { "status_message" : null
+ , "status_code" : 200
+ , "error_info" : null
+ , "response_type" : "OK"
+ , "human_readable_message" : "Found 15 orders"
+ , "response_display_duration_type" : "REGULAR",
+ "about_orders" :
+ [{ "order_oid" : 88833465
+ , "domain_name" : "api.ivan.net.nz"
+ , "slug" : "order-88833465-api-ivan-net-nz"
+ , "billing_oid" : 96122465
+ , "is_on_customers_own_physical_server" : false
+ , "vps_parameters" : { "memory_mb" : 160
+ , "disk_space_mb" : 4096
+ , "disk_space_2_mb" : 0}
+ , "host_server_oid" : "764"
+ , "server_type" : "VPS"
+ , "data_transfer_allowance" : { "data_transfer_gb" : 30
+ , "data_transfer" : "30"}
+ , "billing_info" : { }
+ , "allocated_ips" : { "primary_ip" : "1.2.3.4"
+ , "secondary_ips" : ["1.2.3.5","1.2.3.6"]}
+ , "running_state" : "RUNNING"}]}}"""
+
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+ def _r_pricing_plans(self,method,url,body,headers):
+ body = """
+ {"get_pricing_plans_response" :
+ { "status_message" : null
+ , "status_code" : 200
+ , "error_info" : null
+ , "response_type" : "OK"
+ , "human_readable_message" : "Here some pricing plans we are offering on new orders. Note we offer most disk and memory sizes. So if you setup a new server feel free to vary these (e.g. different memory, disk, etc) and we will just adjust the pricing to suit. Pricing is in USD. If you are an NZ-based customer then we would need to add GST."
+ , "response_display_duration_type" : "REGULAR"
+ , "pricing_plan_infos" :
+ [{ "pricing_plan_code" : "MiroVPSLowContention"
+ , "pricing_plan_description" : "MiroVPS Semi-Dedicated Server (Dallas)"
+ , "monthly_recurring_fee" : 32.54
+ , "monthly_recurring_amt" : { "amt" : 35.0
+ , "currency" : "CUR_AUD"
+ ,"amt_usd" : 32.54}
+ , "minimum_memory_mb" : 950
+ , "minimum_disk_gb" : 20
+ , "minimum_data_transfer_allowance_gb" : 75
+ , "see_also_url" : "http://rimuhosting.com/order/serverdetails.jsp?plan=MiroVPSLowContention"
+ , "server_type" : "VPS"
+ , "offered_at_data_center" :
+ { "data_center_location_code" : "DCDALLAS"
+ , "data_center_location_name" : "Dallas"}}
+ ]}}
+
+ """
+
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _r_distributions(self, method, url, body, headers):
+ body = """
+ { "get_distros_response" : { "status_message" : null
+ , "status_code" : 200
+ , "error_info" : null
+ , "response_type" : "OK"
+ , "human_readable_message" : "Here are the distros we are offering on new orders."
+ , "response_display_duration_type" : "REGULAR"
+ , "distro_infos" : [{ "distro_code" : "lenny"
+ , "distro_description" : "Debian 5.0 (aka Lenny, RimuHosting recommended distro)"}
+ , { "distro_code" : "centos5"
+ , "distro_description" : "Centos5"}
+ , { "distro_code" : "ubuntu904"
+ , "distro_description" : "Ubuntu 9.04 (Jaunty Jackalope, from 2009-04)"}
+ , { "distro_code" : "ubuntu804"
+ , "distro_description" : "Ubuntu 8.04 (Hardy Heron, 5 yr long term support (LTS))"}
+ , { "distro_code" : "ubuntu810"
+ , "distro_description" : "Ubuntu 8.10 (Intrepid Ibex, from 2008-10)"}
+ , { "distro_code" : "fedora10"
+ , "distro_description" : "Fedora 10"}]}}
+ """
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _r_orders_new_vps(self, method, url, body, headers):
+ body = """
+ { "post_new_vps_response" :
+ { "status_message" : null
+ , "status_code" : 200
+ , "error_info" : null
+ , "response_type" : "OK"
+ , "human_readable_message" : null
+ , "response_display_duration_type" : "REGULAR"
+ , "setup_messages" :
+ ["Using user-specified billing data: Wire Transfer" , "Selected user as the owner of the billing details: Ivan Meredith"
+ , "No VPS paramters provided, using default values."]
+ , "about_order" :
+ { "order_oid" : 52255865
+ , "domain_name" : "api.ivan.net.nz"
+ , "slug" : "order-52255865-api-ivan-net-nz"
+ , "billing_oid" : 96122465
+ , "is_on_customers_own_physical_server" : false
+ , "vps_parameters" :
+ { "memory_mb" : 160
+ , "disk_space_mb" : 4096
+ , "disk_space_2_mb" : 0}
+ , "host_server_oid" : "764"
+ , "server_type" : "VPS"
+ , "data_transfer_allowance" :
+ { "data_transfer_gb" : 30 , "data_transfer" : "30"}
+ , "billing_info" : { }
+ , "allocated_ips" :
+ { "primary_ip" : "74.50.57.80", "secondary_ips" : []}
+ , "running_state" : "RUNNING"}
+ , "new_order_request" :
+ { "billing_oid" : 96122465
+ , "user_oid" : 0
+ , "host_server_oid" : null
+ , "vps_order_oid_to_clone" : 0
+ , "ip_request" :
+ { "num_ips" : 1, "extra_ip_reason" : ""}
+ , "vps_parameters" :
+ { "memory_mb" : 160
+ , "disk_space_mb" : 4096
+ , "disk_space_2_mb" : 0}
+ , "pricing_plan_code" : "MIRO1B"
+ , "instantiation_options" :
+ { "control_panel" : "webmin"
+ , "domain_name" : "api.ivan.net.nz"
+ , "password" : "aruxauce27"
+ , "distro" : "lenny"}}
+ , "running_vps_info" :
+ { "pings_ok" : true
+ , "current_kernel" : "default"
+ , "current_kernel_canonical" : "2.6.30.5-xenU.i386"
+ , "last_backup_message" : ""
+ , "is_console_login_enabled" : false
+ , "console_public_authorized_keys" : null
+ , "is_backup_running" : false
+ , "is_backups_enabled" : true
+ , "next_backup_time" :
+ { "ms_since_epoch": 1256446800000, "iso_format" : "2009-10-25T05:00:00Z", "users_tz_offset_ms" : 46800000}
+ , "vps_uptime_s" : 31
+ , "vps_cpu_time_s" : 6
+ , "running_state" : "RUNNING"
+ , "is_suspended" : false}}}
+
+ """
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _r_orders_order_88833465_api_ivan_net_nz_vps(self, method, url, body, headers):
+ body = """
+ { "delete_server_response" :
+ { "status_message" : null
+ , "status_code" : 200
+ , "error_info" : null
+ , "response_type" : "OK"
+ , "human_readable_message" : "Server removed"
+ , "response_display_duration_type" : "REGULAR"
+ , "cancel_messages" :
+ ["api.ivan.net.nz is being shut down."
+ , "A $7.98 credit has been added to your account."
+ , "If you need to un-cancel the server please contact our support team."]
+ }
+ }
+
+ """
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _r_orders_order_88833465_api_ivan_net_nz_vps_running_state(self, method,
+ url, body,
+ headers):
+
+ body = """
+ { "put_running_state_response" :
+ { "status_message" : null
+ , "status_code" : 200
+ , "error_info" : null
+ , "response_type" : "OK"
+ , "human_readable_message" : "api.ivan.net.nz restarted. After the reboot api.ivan.net.nz is pinging OK."
+ , "response_display_duration_type" : "REGULAR"
+ , "is_restarted" : true
+ , "is_pinging" : true
+ , "running_vps_info" :
+ { "pings_ok" : true
+ , "current_kernel" : "default"
+ , "current_kernel_canonical" : "2.6.30.5-xenU.i386"
+ , "last_backup_message" : ""
+ , "is_console_login_enabled" : false
+ , "console_public_authorized_keys" : null
+ , "is_backup_running" : false
+ , "is_backups_enabled" : true
+ , "next_backup_time" :
+ { "ms_since_epoch": 1256446800000, "iso_format" : "2009-10-25T05:00:00Z", "users_tz_offset_ms" : 46800000}
+ , "vps_uptime_s" : 19
+ , "vps_cpu_time_s" : 5
+ , "running_state" : "RUNNING"
+ , "is_suspended" : false}
+ , "host_server_info" : { "is_host64_bit_capable" : true
+ , "default_kernel_i386" : "2.6.30.5-xenU.i386"
+ , "default_kernel_x86_64" : "2.6.30.5-xenU.x86_64"
+ , "cpu_model_name" : "Intel(R) Xeon(R) CPU E5506 @ 2.13GHz"
+ , "host_num_cores" : 1
+ , "host_xen_version" : "3.4.1"
+ , "hostload" : [1.45
+ , 0.56
+ , 0.28]
+ , "host_uptime_s" : 3378276
+ , "host_mem_mb_free" : 51825
+ , "host_mem_mb_total" : 73719
+ , "running_vpss" : 34}
+ , "running_state_messages" : null}}
+
+ """
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
diff --git a/test/test_slicehost.py b/test/test_slicehost.py
new file mode 100644
index 0000000..20dcb95
--- /dev/null
+++ b/test/test_slicehost.py
@@ -0,0 +1,290 @@
+# 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.
+# libcloud.org 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.
+import unittest
+
+from libcloud.drivers.slicehost import SlicehostNodeDriver as Slicehost
+from libcloud.types import Provider, NodeState
+from libcloud.base import Node, NodeImage, NodeSize
+
+import httplib
+
+from test import MockHttp, multipleresponse, TestCaseMixin
+from secrets import SLICEHOST_KEY
+from xml.etree import ElementTree as ET
+
+class SlicehostTest(unittest.TestCase, TestCaseMixin):
+
+ def setUp(self):
+
+ Slicehost.connectionCls.conn_classes = (None, SlicehostMockHttp)
+ SlicehostMockHttp.type = None
+ self.driver = Slicehost('foo')
+ #self.driver = Slicehost(SLICEHOST_KEY)
+
+ def test_list_nodes(self):
+ ret = self.driver.list_nodes()
+ self.assertEqual(len(ret), 1)
+ node = ret[0]
+ self.assertTrue('174.143.212.229' in node.public_ip)
+ self.assertTrue('10.176.164.199' in node.private_ip)
+ self.assertEqual(node.state, NodeState.PENDING)
+
+ SlicehostMockHttp.type = 'UNAUTHORIZED'
+ try:
+ ret = self.driver.list_nodes()
+ except Exception, e:
+ self.assertEqual(e.args[0], 'HTTP Basic: Access denied.')
+ else:
+ self.fail('test should have thrown')
+
+ def test_list_sizes(self):
+ ret = self.driver.list_sizes()
+ self.assertEqual(len(ret), 7)
+ size = ret[0]
+ self.assertEqual(size.name, '256 slice')
+
+ def test_list_images(self):
+ ret = self.driver.list_images()
+ self.assertEqual(len(ret), 11)
+ image = ret[0]
+ self.assertEqual(image.name, 'CentOS 5.2')
+ self.assertEqual(image.id, 2)
+
+ def test_reboot_node(self):
+ node = Node(id=1, name=None, state=None, public_ip=None, private_ip=None,
+ driver=self.driver)
+
+ ret = node.reboot()
+ self.assertTrue(ret is True)
+
+ ret = self.driver.reboot_node(node)
+ self.assertTrue(ret is True)
+
+ SlicehostMockHttp.type = 'FORBIDDEN'
+ try:
+ ret = self.driver.reboot_node(node)
+ except Exception, e:
+ self.assertEqual(e.args[0], 'Permission denied')
+ else:
+ self.fail('test should have thrown')
+
+ def test_destroy_node(self):
+ node = Node(id=1, name=None, state=None, public_ip=None, private_ip=None,
+ driver=self.driver)
+
+ ret = node.destroy()
+ self.assertTrue(ret is True)
+
+ ret = self.driver.destroy_node(node)
+ self.assertTrue(ret is True)
+
+ def test_create_node(self):
+ image = NodeImage(id=11, name='ubuntu 8.10', driver=self.driver)
+ size = NodeSize(1, '256 slice', None, None, None, None, driver=self.driver)
+ node = self.driver.create_node(name='slicetest', image=image, size=size)
+ self.assertEqual(node.name, 'slicetest')
+
+class SlicehostMockHttp(MockHttp):
+
+ def _slices_xml(self, method, url, body, headers):
+ if method == 'POST':
+ tree = ET.XML(body)
+ name = tree.findtext('name')
+ image_id = int(tree.findtext('image-id'))
+ flavor_id = int(tree.findtext('flavor-id'))
+
+ # TODO: would be awesome to get the slicehost api developers to fill in the
+ # the correct validation logic
+ if not (name and image_id and flavor_id) \
+ or tree.tag != 'slice' \
+ or not headers.has_key('Content-Type') \
+ or headers['Content-Type'] != 'application/xml':
+ err_body = """<?xml version="1.0" encoding="UTF-8"?>
+<errors>
+ <error>Slice parameters are not properly nested</error>
+</errors>"""
+ return (httplib.UNPROCESSABLE_ENTITY, err_body, {}, '')
+
+ body = """<slice>
+ <name>slicetest</name>
+ <image-id type="integer">11</image-id>
+ <addresses type="array">
+ <address>10.176.168.15</address>
+ <address>67.23.20.114</address>
+ </addresses>
+ <root-password>fooadfa1231</root-password>
+ <progress type="integer">0</progress>
+ <id type="integer">71907</id>
+ <bw-out type="float">0.0</bw-out>
+ <bw-in type="float">0.0</bw-in>
+ <flavor-id type="integer">1</flavor-id>
+ <status>build</status>
+ <ip-address>10.176.168.15</ip-address>
+</slice>"""
+ return (httplib.CREATED, body, {}, '')
+ else:
+ body = """<slices type="array">
+ <slice>
+ <name>libcloud-foo</name>
+ <image-id type="integer">10</image-id>
+ <addresses type="array">
+ <address>174.143.212.229</address>
+ <address>10.176.164.199</address>
+ </addresses>
+ <progress type="integer">0</progress>
+ <id type="integer">1</id>
+ <bw-out type="float">0.0</bw-out>
+ <bw-in type="float">0.0</bw-in>
+ <flavor-id type="integer">1</flavor-id>
+ <status>build</status>
+ <ip-address>174.143.212.229</ip-address>
+ </slice>
+</slices>"""
+
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _slices_xml_UNAUTHORIZED(self, method, url, body, headers):
+ err_body = 'HTTP Basic: Access denied.'
+ return (httplib.UNAUTHORIZED, err_body, {},
+ httplib.responses[httplib.UNAUTHORIZED])
+
+ def _flavors_xml(self, method, url, body, headers):
+ body = """<?xml version="1.0" encoding="UTF-8"?>
+<flavors type="array">
+ <flavor>
+ <id type="integer">1</id>
+ <name>256 slice</name>
+ <price type="integer">2000</price>
+ <ram type="integer">256</ram>
+ </flavor>
+ <flavor>
+ <id type="integer">2</id>
+ <name>512 slice</name>
+ <price type="integer">3800</price>
+ <ram type="integer">512</ram>
+ </flavor>
+ <flavor>
+ <id type="integer">3</id>
+ <name>1GB slice</name>
+ <price type="integer">7000</price>
+ <ram type="integer">1024</ram>
+ </flavor>
+ <flavor>
+ <id type="integer">4</id>
+ <name>2GB slice</name>
+ <price type="integer">13000</price>
+ <ram type="integer">2048</ram>
+ </flavor>
+ <flavor>
+ <id type="integer">5</id>
+ <name>4GB slice</name>
+ <price type="integer">25000</price>
+ <ram type="integer">4096</ram>
+ </flavor>
+ <flavor>
+ <id type="integer">6</id>
+ <name>8GB slice</name>
+ <price type="integer">45000</price>
+ <ram type="integer">8192</ram>
+ </flavor>
+ <flavor>
+ <id type="integer">7</id>
+ <name>15.5GB slice</name>
+ <price type="integer">80000</price>
+ <ram type="integer">15872</ram>
+ </flavor>
+</flavors>
+"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _images_xml(self, method, url, body, headers):
+ body = """<?xml version="1.0" encoding="UTF-8"?>
+<images type="array">
+ <image>
+ <name>CentOS 5.2</name>
+ <id type="integer">2</id>
+ </image>
+ <image>
+ <name>Gentoo 2008.0</name>
+ <id type="integer">3</id>
+ </image>
+ <image>
+ <name>Debian 5.0 (lenny)</name>
+ <id type="integer">4</id>
+ </image>
+ <image>
+ <name>Fedora 10 (Cambridge)</name>
+ <id type="integer">5</id>
+ </image>
+ <image>
+ <name>CentOS 5.3</name>
+ <id type="integer">7</id>
+ </image>
+ <image>
+ <name>Ubuntu 9.04 (jaunty)</name>
+ <id type="integer">8</id>
+ </image>
+ <image>
+ <name>Arch 2009.02</name>
+ <id type="integer">9</id>
+ </image>
+ <image>
+ <name>Ubuntu 8.04.2 LTS (hardy)</name>
+ <id type="integer">10</id>
+ </image>
+ <image>
+ <name>Ubuntu 8.10 (intrepid)</name>
+ <id type="integer">11</id>
+ </image>
+ <image>
+ <name>Red Hat EL 5.3</name>
+ <id type="integer">12</id>
+ </image>
+ <image>
+ <name>Fedora 11 (Leonidas)</name>
+ <id type="integer">13</id>
+ </image>
+</images>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _slices_1_reboot_xml(self, method, url, body, headers):
+ body = """<slice>
+ <name>libcloud-test</name>
+ <image-id type="integer">10</image-id>
+ <addresses type="array">
+ <address>174.143.212.229</address>
+ <address>10.176.164.199</address>
+ </addresses>
+ <progress type="integer">100</progress>
+ <id type="integer">70507</id>
+ <bw-out type="float">0.0</bw-out>
+ <bw-in type="float">0.0</bw-in>
+ <flavor-id type="integer">1</flavor-id>
+ <status>reboot</status>
+ <ip-address>174.143.212.229</ip-address>
+</slice>"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+
+ def _slices_1_reboot_xml_FORBIDDEN(self, method, url, body, headers):
+ err_body = """<errors>
+ <error>Permission denied</error>
+</errors>"""
+ return (httplib.FORBIDDEN, err_body, {},
+ httplib.responses[httplib.FORBIDDEN])
+
+ def _slices_1_destroy_xml(self, method, url, body, headers):
+ body = ''
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
diff --git a/test/test_vcloud.py b/test/test_vcloud.py
new file mode 100644
index 0000000..0d511b5
--- /dev/null
+++ b/test/test_vcloud.py
@@ -0,0 +1,329 @@
+# 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.
+# libcloud.org 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.
+import unittest
+import exceptions
+
+from libcloud.drivers.vcloud import TerremarkDriver
+from libcloud.drivers.vcloud import VCloudNodeDriver
+from libcloud.base import Node, NodeImage, NodeSize
+from libcloud.types import NodeState
+
+from test import MockHttp, TestCaseMixin
+
+import httplib
+
+from secrets import TERREMARK_USER, TERREMARK_SECRET
+
+class TerremarkTests(unittest.TestCase, TestCaseMixin):
+
+ def setUp(self):
+ VCloudNodeDriver.connectionCls.host = "test"
+ VCloudNodeDriver.connectionCls.conn_classes = (None, TerremarkMockHttp)
+ TerremarkMockHttp.type = None
+ self.driver = TerremarkDriver(TERREMARK_USER, TERREMARK_SECRET)
+
+ def test_list_images(self):
+ ret = self.driver.list_images()
+ self.assertEqual(ret[0].id,'https://services.vcloudexpress.terremark.com/api/v0.8/vAppTemplate/5')
+
+ def test_list_sizes(self):
+ ret = self.driver.list_sizes()
+ self.assertEqual(ret[0].ram, 512)
+
+ def test_create_node(self):
+ image = self.driver.list_images()[0]
+ size = self.driver.list_sizes()[0]
+ node = self.driver.create_node(
+ name='testerpart2',
+ image=image,
+ size=size,
+ vdc='https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224',
+ network='https://services.vcloudexpress.terremark.com/api/v0.8/network/725',
+ cpus=2,
+ )
+ self.assertTrue(isinstance(node, Node))
+ self.assertEqual(node.id, 'https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031')
+ self.assertEqual(node.name, 'testerpart2')
+
+ def test_list_nodes(self):
+ ret = self.driver.list_nodes()
+ node = ret[0]
+ self.assertEqual(node.id, 'https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031')
+ self.assertEqual(node.name, 'testerpart2')
+ self.assertEqual(node.state, NodeState.RUNNING)
+ self.assertEqual(node.public_ip, [])
+ self.assertEqual(node.private_ip, ['10.112.78.69'])
+
+ def test_reboot_node(self):
+ node = self.driver.list_nodes()[0]
+ ret = self.driver.reboot_node(node)
+ self.assertTrue(ret)
+
+ def test_destroy_node(self):
+ node = self.driver.list_nodes()[0]
+ ret = self.driver.destroy_node(node)
+ self.assertTrue(ret)
+
+
+class TerremarkMockHttp(MockHttp):
+
+ def _api_v0_8_login(self, method, url, body, headers):
+ headers['set-cookie'] = 'vcloud-token=testtoken'
+ body = """<OrgList xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Org href="https://services.vcloudexpress.terremark.com/api/v0.8/org/240" type="application/vnd.vmware.vcloud.org+xml" name="a@example.com"/>
+</OrgList>
+"""
+ return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+ def _api_v0_8_org_240(self, method, url, body, headers):
+ body = """<Org href="https://services.vcloudexpress.terremark.com/api/v0.8/org/240" name="a@example.com" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/catalog" type="application/vnd.vmware.vcloud.catalog+xml" name="Miami Environment 1 Catalog"/>
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/tasksList/224" type="application/vnd.vmware.vcloud.tasksList+xml" name="Miami Environment 1 Tasks List"/>
+</Org>
+"""
+ return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+ def _api_v0_8_vdc_224(self, method, url, body, headers):
+ body = """<Vdc href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/catalog" type="application/vnd.vmware.vcloud.catalog+xml" name="Miami Environment 1"/>
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/publicIps" type="application/xml" name="Public IPs"/>
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/internetServices" type="application/xml" name="Internet Services"/>
+ <Description/>
+ <ResourceEntities>
+ <ResourceEntity href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+ </ResourceEntities>
+ <AvailableNetworks>
+ <Network href="https://services.vcloudexpress.terremark.com/api/v0.8/network/725" type="application/vnd.vmware.vcloud.network+xml" name="10.112.78.64/26"/>
+ </AvailableNetworks>
+</Vdc>
+"""
+ return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+ def _api_v0_8_vdc_224_catalog(self, method, url, body, headers):
+ body = """<Catalog href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/catalog" type="application/vnd.vmware.vcloud.catalog+xml" name="Miami Environment 1" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <CatalogItems>
+ <CatalogItem href="https://services.vcloudexpress.terremark.com/api/v0.8/catalogItem/5" type="application/vnd.vmware.vcloud.catalogItem+xml" name="CentOS 5.3 (32-bit)"/>
+ </CatalogItems>
+</Catalog>
+"""
+ return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+ def _api_v0_8_catalogItem_5(self, method, url, body, headers):
+ body = """<CatalogItem href="https://services.vcloudexpress.terremark.com/api/v0.8/catalogItem/5" type="application/vnd.vmware.vcloud.catalogItem+xml" name="CentOS 5.3 (32-bit)" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/catalogItem/5/options/compute" type="application/xml" name="Compute Options"/>
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/catalogItem/5/options/customization" type="application/xml" name="Customization Options"/>
+ <Entity href="https://services.vcloudexpress.terremark.com/api/v0.8/vAppTemplate/5" type="application/vnd.vmware.vcloud.vAppTemplate+xml" name="CentOS 5.3 (32-bit)"/>
+ <Property key="LicensingCost">0</Property>
+</CatalogItem>
+"""
+ return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+ def _api_v0_8_vdc_224_action_instantiateVAppTemplate(self, method, url, body, headers):
+ body = """<VApp href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2" status="0" size="10" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Link rel="up" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml"/>
+</VApp>
+"""
+ return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+ def _api_v0_8_vapp_14031_action_deploy(self, method, url, body, headers):
+ body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/10496" type="application/vnd.vmware.vcloud.task+xml" status="queued" startTime="2009-11-13T23:58:22.893Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+ <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+ return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+ def _api_v0_8_task_10496(self, method, url, body, headers):
+ body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/10496" type="application/vnd.vmware.vcloud.task+xml" status="success" startTime="2009-11-13T23:58:22.893Z" endTime="2009-11-14T00:01:02.507Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+ <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+ return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+ def _api_v0_8_vapp_14031_power_action_powerOn(self, method, url, body, headers):
+ body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/10499" type="application/vnd.vmware.vcloud.task+xml" status="queued" startTime="2009-11-14T00:01:05.227Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+ <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+
+"""
+ return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+ def _api_v0_8_vapp_14031(self, method, url, body, headers):
+ if method == 'GET':
+ body = """<VApp href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2" status="4" size="10485760" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Link rel="up" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml"/>
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031/options/compute" type="application/xml" name="Compute Options"/>
+ <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031/options/customization" type="application/xml" name="Customization Options"/>
+ <NetworkConnectionSection xmlns="http://schemas.dmtf.org/ovf/envelope/1">
+ <NetworkConnection Network="Internal" xmlns="http://www.vmware.com/vcloud/v0.8">
+ <IpAddress>10.112.78.69</IpAddress>
+ </NetworkConnection>
+ </NetworkConnectionSection>
+ <OperatingSystemSection d2p1:id="25" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:d2p1="http://schemas.dmtf.org/ovf/envelope/1">
+ <Info>The kind of installed guest operating system</Info>
+ <Description>Red Hat Enterprise Linux 5 (32-bit)</Description>
+ </OperatingSystemSection>
+ <VirtualHardwareSection xmlns="http://schemas.dmtf.org/ovf/envelope/1">
+ <Info>Virtual Hardware</Info>
+ <System>
+ <AutomaticRecoveryAction xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <AutomaticShutdownAction xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <AutomaticStartupAction xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <AutomaticStartupActionDelay xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <AutomaticStartupActionSequenceNumber xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <ConfigurationDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <ConfigurationFile xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <ConfigurationID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <CreationTime xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <Description xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">Virtual Hardware Family</ElementName>
+ <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">0</InstanceID>
+ <LogDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <RecoveryFile xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <SnapshotDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <SuspendDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <SwapFileDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+ <VirtualSystemIdentifier xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">testerpart2</VirtualSystemIdentifier>
+ <VirtualSystemType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">vmx-07</VirtualSystemType>
+ </System>
+ <Item>
+ <Address xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AddressOnParent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AllocationUnits xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">hertz * 10^6</AllocationUnits>
+ <AutomaticAllocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AutomaticDeallocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ConsumerVisibility xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Description xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">Number of Virtual CPUs</Description>
+ <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">2 virtual CPU(s)</ElementName>
+ <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">1</InstanceID>
+ <Limit xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <MappingBehavior xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <OtherResourceType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Parent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <PoolID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Reservation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ResourceSubType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ResourceType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">3</ResourceType>
+ <VirtualQuantity xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">2</VirtualQuantity>
+ <VirtualQuantityUnits xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">count</VirtualQuantityUnits>
+ <Weight xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ </Item>
+ <Item>
+ <Address xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AddressOnParent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AllocationUnits xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">byte * 2^20</AllocationUnits>
+ <AutomaticAllocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AutomaticDeallocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ConsumerVisibility xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Description xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">Memory Size</Description>
+ <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">512MB of memory</ElementName>
+ <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">2</InstanceID>
+ <Limit xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <MappingBehavior xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <OtherResourceType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Parent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <PoolID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Reservation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ResourceSubType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ResourceType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">4</ResourceType>
+ <VirtualQuantity xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">512</VirtualQuantity>
+ <VirtualQuantityUnits xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">byte * 2^20</VirtualQuantityUnits>
+ <Weight xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ </Item>
+ <Item>
+ <Address xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">0</Address>
+ <AddressOnParent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AllocationUnits xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AutomaticAllocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AutomaticDeallocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ConsumerVisibility xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Description xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">SCSI Controller</Description>
+ <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">SCSI Controller 0</ElementName>
+ <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">3</InstanceID>
+ <Limit xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <MappingBehavior xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <OtherResourceType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Parent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <PoolID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Reservation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ResourceSubType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">lsilogic</ResourceSubType>
+ <ResourceType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">6</ResourceType>
+ <VirtualQuantity xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <VirtualQuantityUnits xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Weight xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ </Item>
+ <Item>
+ <Address xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AddressOnParent xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">0</AddressOnParent>
+ <AllocationUnits xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AutomaticAllocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <AutomaticDeallocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ConsumerVisibility xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Description xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">Hard Disk 1</ElementName>
+ <HostResource xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">10485760</HostResource>
+ <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">9</InstanceID>
+ <Limit xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <MappingBehavior xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <OtherResourceType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Parent xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">3</Parent>
+ <PoolID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Reservation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ResourceSubType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <ResourceType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">17</ResourceType>
+ <VirtualQuantity xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">10485760</VirtualQuantity>
+ <VirtualQuantityUnits xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ <Weight xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+ </Item>
+ </VirtualHardwareSection>
+</VApp>
+"""
+ elif method == 'DELETE':
+ body = ''
+ return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+ def _api_v0_8_vapp_14031_power_action_reset(self, method, url, body, headers):
+ body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/10555" type="application/vnd.vmware.vcloud.task+xml" status="queued" startTime="2009-11-14T00:54:50.417Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+ <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+ return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+ def _api_v0_8_vapp_14031_power_action_poweroff(self, method, url, body, headers):
+ body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/11001" type="application/vnd.vmware.vcloud.task+xml" status="queued" startTime="2009-11-16T18:18:02.82Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+ <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+ return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+ def _api_v0_8_task_11001(self, method, url, body, headers):
+ body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/11001" type="application/vnd.vmware.vcloud.task+xml" status="success" startTime="2009-11-16T18:18:02.82Z" endTime="2009-11-16T18:18:17.567Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+ <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+ return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+
diff --git a/test/test_voxel.py b/test/test_voxel.py
new file mode 100644
index 0000000..15dd0e9
--- /dev/null
+++ b/test/test_voxel.py
@@ -0,0 +1,50 @@
+# 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.
+# libcloud.org 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.
+import unittest
+
+from libcloud.drivers.voxel import VoxelNodeDriver as Voxel
+from libcloud.types import Provider, NodeState, InvalidCredsException
+from libcloud.base import Node, NodeImage, NodeSize
+
+import httplib
+
+from test import MockHttp, multipleresponse, TestCaseMixin
+from secrets import VOXEL_USER, VOXEL_SECRET
+from xml.etree import ElementTree as ET
+
+class VoxelTest(unittest.TestCase):
+
+ def setUp(self):
+
+ Voxel.connectionCls.conn_classes = (None, VoxelMockHttp)
+ VoxelMockHttp.type = None
+ self.driver = Voxel('foo', 'bar')
+
+ def test_auth_failed(self):
+ VoxelMockHttp.type = 'UNAUTHORIZED'
+ try:
+ ret = self.driver.list_nodes()
+ except Exception, e:
+ self.assertTrue(isinstance(e, InvalidCredsException))
+ else:
+ self.fail('test should have thrown')
+
+class VoxelMockHttp(MockHttp):
+
+ def _UNAUTHORIZED(self, method, url, body, headers):
+ body = """<?xml version="1.0"?>
+<rsp stat="fail"><err code="1" msg="Invalid login or password"/><method>voxel.devices.list</method><parameters><param name="timestamp">2010-02-10T23:39:25.808107+0000</param><param name="key">authshouldfail</param><param name="api_sig">ae069bb835e998622caaddaeff8c98e0</param></parameters><string_to_sign>YOUR_SECRETtimestamp2010-02-10T23:39:25.808107+0000methodvoxel.devices.listkeyauthshouldfail</string_to_sign></rsp>
+"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
diff --git a/test/test_vpsnet.py b/test/test_vpsnet.py
new file mode 100644
index 0000000..52bd77c
--- /dev/null
+++ b/test/test_vpsnet.py
@@ -0,0 +1,205 @@
+# 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.
+# libcloud.org 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.
+import unittest
+import exceptions
+
+from libcloud.drivers.vpsnet import VPSNetNodeDriver
+from libcloud.base import Node, NodeImage, NodeSize
+from libcloud.types import NodeState
+
+from test import MockHttp, TestCaseMixin
+
+import httplib
+
+from secrets import VPSNET_USER, VPSNET_KEY
+
+class VPSNetTests(unittest.TestCase, TestCaseMixin):
+
+ def setUp(self):
+ VPSNetNodeDriver.connectionCls.conn_classes = (None, VPSNetMockHttp)
+ self.driver = VPSNetNodeDriver(VPSNET_USER, VPSNET_KEY)
+
+ def test_create_node(self):
+ VPSNetMockHttp.type = 'create'
+ image = self.driver.list_images()[0]
+ size = self.driver.list_sizes()[0]
+ node = self.driver.create_node('foo', image, size)
+ self.assertEqual(node.name, 'foo')
+
+ def test_list_nodes(self):
+ VPSNetMockHttp.type = 'virtual_machines'
+ node = self.driver.list_nodes()[0]
+ self.assertEqual(node.id, 1384)
+ self.assertEqual(node.state, NodeState.RUNNING)
+
+ def test_reboot_node(self):
+ VPSNetMockHttp.type = 'virtual_machines'
+ node = self.driver.list_nodes()[0]
+
+ VPSNetMockHttp.type = 'reboot'
+ ret = self.driver.reboot_node(node)
+ self.assertEqual(ret, True)
+
+ def test_destroy_node(self):
+ VPSNetMockHttp.type = 'delete'
+ node = Node('2222', None, None, None, None, self.driver)
+ ret = self.driver.destroy_node(node)
+ self.assertTrue(ret)
+ VPSNetMockHttp.type = 'delete_fail'
+ node = Node('2223', None, None, None, None, self.driver)
+ self.assertRaises(exceptions.Exception, self.driver.destroy_node, node)
+
+ def test_list_images(self):
+ VPSNetMockHttp.type = 'templates'
+ ret = self.driver.list_images()
+ self.assertEqual(ret[0].id, 9)
+ self.assertEqual(ret[-1].id, 160)
+
+ def test_list_sizes(self):
+ VPSNetMockHttp.type = 'sizes'
+ ret = self.driver.list_sizes()
+ self.assertEqual(len(ret), 2)
+ self.assertEqual(ret[1].id, 2)
+ self.assertEqual(ret[1].name, '2 Node')
+
+ def test_destroy_node_response(self):
+ # should return a node object
+ node = Node('2222', None, None, None, None, self.driver)
+ VPSNetMockHttp.type = 'delete'
+ ret = self.driver.destroy_node(node)
+ self.assertTrue(isinstance(ret, bool))
+
+ def test_reboot_node_response(self):
+ # should return a node object
+ VPSNetMockHttp.type = 'virtual_machines'
+ node = self.driver.list_nodes()[0]
+ VPSNetMockHttp.type = 'reboot'
+ ret = self.driver.reboot_node(node)
+ self.assertTrue(isinstance(ret, bool))
+
+
+
+class VPSNetMockHttp(MockHttp):
+
+
+ def _nodes_api10json_sizes(self, method, url, body, headers):
+ body = """[{"slice":{"virtual_machine_id":8592,"id":12256,"consumer_id":0}},
+ {"slice":{"virtual_machine_id":null,"id":12258,"consumer_id":0}},
+ {"slice":{"virtual_machine_id":null,"id":12434,"consumer_id":0}}]"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _nodes_api10json_create(self, method, url, body, headers):
+ body = """[{"slice":{"virtual_machine_id":8592,"id":12256,"consumer_id":0}},
+ {"slice":{"virtual_machine_id":null,"id":12258,"consumer_id":0}},
+ {"slice":{"virtual_machine_id":null,"id":12434,"consumer_id":0}}]"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _virtual_machines_2222_api10json_delete_fail(self, method, url, body, headers):
+ return (httplib.FORBIDDEN, '', {}, httplib.responses[httplib.FORBIDDEN])
+
+ def _virtual_machines_2222_api10json_delete(self, method, url, body, headers):
+ return (httplib.OK, '', {}, httplib.responses[httplib.OK])
+
+ def _virtual_machines_1384_reboot_api10json_reboot(self, method, url, body, headers):
+ body = """{
+ "virtual_machine":
+ {
+ "running": true,
+ "updated_at": "2009-05-15T06:55:02-04:00",
+ "power_action_pending": false,
+ "system_template_id": 41,
+ "id": 1384,
+ "cloud_id": 3,
+ "domain_name": "demodomain.com",
+ "hostname": "web01",
+ "consumer_id": 0,
+ "backups_enabled": false,
+ "password": "a8hjsjnbs91",
+ "label": "foo",
+ "slices_count": null,
+ "created_at": "2009-04-16T08:17:39-04:00"
+ }
+ }"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _virtual_machines_api10json_create(self, method, url, body, headers):
+ body = """{
+ "virtual_machine":
+ {
+ "running": true,
+ "updated_at": "2009-05-15T06:55:02-04:00",
+ "power_action_pending": false,
+ "system_template_id": 41,
+ "id": 1384,
+ "cloud_id": 3,
+ "domain_name": "demodomain.com",
+ "hostname": "web01",
+ "consumer_id": 0,
+ "backups_enabled": false,
+ "password": "a8hjsjnbs91",
+ "label": "foo",
+ "slices_count": null,
+ "created_at": "2009-04-16T08:17:39-04:00"
+ }
+ }"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _virtual_machines_api10json_virtual_machines(self, method, url, body, headers):
+ body = """ [{
+ "virtual_machine":
+ {
+ "running": true,
+ "updated_at": "2009-05-15T06:55:02-04:00",
+ "power_action_pending": false,
+ "system_template_id": 41,
+ "id": 1384,
+ "cloud_id": 3,
+ "domain_name": "demodomain.com",
+ "hostname": "web01",
+ "consumer_id": 0,
+ "backups_enabled": false,
+ "password": "a8hjsjnbs91",
+ "label": "Web Server 01",
+ "slices_count": null,
+ "created_at": "2009-04-16T08:17:39-04:00"
+ }
+ },
+ {
+ "virtual_machine":
+ {
+ "running": true,
+ "updated_at": "2009-05-15T06:55:02-04:00",
+ "power_action_pending": false,
+ "system_template_id": 41,
+ "id": 1385,
+ "cloud_id": 3,
+ "domain_name": "demodomain.com",
+ "hostname": "mysql01",
+ "consumer_id": 0,
+ "backups_enabled": false,
+ "password": "dsi8h38hd2s",
+ "label": "MySQL Server 01",
+ "slices_count": null,
+ "created_at": "2009-04-16T08:17:39-04:00"
+ }
+ }]"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+ def _available_clouds_api10json_templates(self, method, url, body, headers):
+ body = """[{"cloud":{"system_templates":[{"id":9,"label":"Ubuntu 8.04 x64"},{"id":10,"label":"CentOS 5.2 x64"},{"id":11,"label":"Gentoo 2008.0 x64"},{"id":18,"label":"Ubuntu 8.04 x64 LAMP"},{"id":19,"label":"Ubuntu 8.04 x64 MySQL"},{"id":20,"label":"Ubuntu 8.04 x64 Postfix"},{"id":21,"label":"Ubuntu 8.04 x64 Apache"},{"id":22,"label":"CentOS 5.2 x64 MySQL"},{"id":23,"label":"CentOS 5.2 x64 LAMP"},{"id":24,"label":"CentOS 5.2 x64 HAProxy"},{"id":25,"label":"CentOS 5.2 x64 Postfix"},{"id":26,"label":"CentOS 5.2 x64 Varnish"},{"id":27,"label":"CentOS 5.2 x64 Shoutcast"},{"id":28,"label":"CentOS 5.2 x64 Apache"},{"id":40,"label":"cPanel"},{"id":42,"label":"Debian 5.0 (Lenny) x64"},{"id":58,"label":"Django on Ubuntu 8.04 (x86)"},{"id":59,"label":"Drupal 5 on Ubuntu 8.04 (x86)"},{"id":60,"label":"Drupal 6 on Ubuntu 8.04 (x86)"},{"id":61,"label":"Google App Engine on Ubuntu 8.04 (x86)"},{"id":62,"label":"LAMP on Ubuntu 8.04 (x86)"},{"id":63,"label":"LAPP on Ubuntu 8.04 (x86)"},{"id":64,"label":"MediaWiki on Ubuntu 8.04 (x86)"},{"id":65,"label":"MySQL on Ubuntu 8.04 (x86)"},{"id":66,"label":"phpBB on Ubuntu 8.04 (x86)"},{"id":67,"label":"PostgreSQL on Ubuntu 8.04 (x86)"},{"id":68,"label":"Rails on Ubuntu 8.04 (x86)"},{"id":69,"label":"Tomcat on Ubuntu 8.04 (x86)"},{"id":70,"label":"Wordpress on Ubuntu 8.04 (x86)"},{"id":71,"label":"Joomla on Ubuntu 8.04 (x86)"},{"id":72,"label":"Ubuntu 8.04 Default Install (turnkey)"},{"id":128,"label":"CentOS Optimised"},{"id":129,"label":"Optimised CentOS + Apache + MySQL + PHP"},{"id":130,"label":"Optimised CentOS + Apache + MySQL + Ruby"},{"id":131,"label":"Optimised CentOS + Apache + MySQL + Ruby + PHP"},{"id":132,"label":"Debian Optimised"},{"id":133,"label":"Optimised Debian + Apache + MySQL + PHP"},{"id":134,"label":"Optimised Debian + NGINX + MySQL + PHP"},{"id":135,"label":"Optimised Debian + Lighttpd + MySQL + PHP"},{"id":136,"label":"Optimised Debian + Apache + MySQL + Ruby + PHP"},{"id":137,"label":"Optimised Debian + Apache + MySQL + Ruby"},{"id":138,"label":"Optimised Debian + NGINX + MySQL + Ruby + PHP"},{"id":139,"label":"Optimised Debian + NGINX + MySQL + Ruby"},{"id":140,"label":"Optimised Debian + Apache + MySQL + PHP + Magento"},{"id":141,"label":"Optimised Debian + NGINX + MySQL + PHP + Magento"},{"id":142,"label":"Optimised Debian + Lighttpd + MySQL + PHP + Wordpress"}],"id":2,"label":"USA VPS Cloud"}},{"cloud":{"system_templates":[{"id":15,"label":"Ubuntu 8.04 x64"},{"id":16,"label":"CentOS 5.2 x64"},{"id":17,"label":"Gentoo 2008.0 x64"},{"id":29,"label":"Ubuntu 8.04 x64 LAMP"},{"id":30,"label":"Ubuntu 8.04 x64 MySQL"},{"id":31,"label":"Ubuntu 8.04 x64 Postfix"},{"id":32,"label":"Ubuntu 8.04 x64 Apache"},{"id":33,"label":"CentOS 5.2 x64 MySQL"},{"id":34,"label":"CentOS 5.2 x64 LAMP"},{"id":35,"label":"CentOS 5.2 x64 HAProxy"},{"id":36,"label":"CentOS 5.2 x64 Postfix"},{"id":37,"label":"CentOS 5.2 x64 Varnish"},{"id":38,"label":"CentOS 5.2 x64 Shoutcast"},{"id":39,"label":"CentOS 5.2 x64 Apache"},{"id":41,"label":"cPanel"},{"id":43,"label":"Debian 5.0 (Lenny) x64"},{"id":44,"label":"Django on Ubuntu 8.04 (x86)"},{"id":45,"label":"Drupal 5 on Ubuntu 8.04 (x86)"},{"id":46,"label":"Drupal 6 on Ubuntu 8.04 (x86)"},{"id":47,"label":"Google App Engine on Ubuntu 8.04 (x86)"},{"id":48,"label":"LAMP on Ubuntu 8.04 (x86)"},{"id":49,"label":"LAPP on Ubuntu 8.04 (x86)"},{"id":50,"label":"MediaWiki on Ubuntu 8.04 (x86)"},{"id":51,"label":"MySQL on Ubuntu 8.04 (x86)"},{"id":52,"label":"phpBB on Ubuntu 8.04 (x86)"},{"id":53,"label":"PostgreSQL on Ubuntu 8.04 (x86)"},{"id":54,"label":"Rails on Ubuntu 8.04 (x86)"},{"id":55,"label":"Tomcat on Ubuntu 8.04 (x86)"},{"id":56,"label":"Wordpress on Ubuntu 8.04 (x86)"},{"id":57,"label":"Joomla on Ubuntu 8.04 (x86)"},{"id":73,"label":"Ubuntu 8.04 Default Install (turnkey)"},{"id":148,"label":"CentOS Optimised"},{"id":149,"label":"Optimised CentOS + Apache + MySQL + PHP"},{"id":150,"label":"Optimised CentOS + Apache + MySQL + Ruby"},{"id":151,"label":"Optimised CentOS + Apache + MySQL + Ruby + PHP"},{"id":152,"label":"Debian Optimised"},{"id":153,"label":"Optimised Debian + Apache + MySQL + PHP"},{"id":154,"label":"Optimised Debian + NGINX + MySQL + PHP"},{"id":155,"label":"Optimised Debian + Lighttpd + MySQL + PHP"},{"id":156,"label":"Optimised Debian + Apache + MySQL + Ruby + PHP"},{"id":157,"label":"Optimised Debian + Apache + MySQL + Ruby"},{"id":158,"label":"Optimised Debian + NGINX + MySQL + Ruby + PHP"},{"id":159,"label":"Optimised Debian + NGINX + MySQL + Ruby"},{"id":160,"label":"Optimised Debian + Lighttpd + MySQL + PHP + Wordpress"}],"id":3,"label":"UK VPS Cloud"}}]"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+ def _available_clouds_api10json_create(self, method, url, body, headers):
+ body = """[{"cloud":{"system_templates":[{"id":9,"label":"Ubuntu 8.04 x64"}],"id":2,"label":"USA VPS Cloud"}}]"""
+ return (httplib.OK, body, {}, httplib.responses[httplib.OK])