Repository: libcloud
Updated Branches:
  refs/heads/trunk abe832fc9 -> 59de189c4


Added Runabove volume management & fixed many little things

Close #561

Signed-off-by: Tomaz Muraus <[email protected]>


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/7bc2d29b
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/7bc2d29b
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/7bc2d29b

Branch: refs/heads/trunk
Commit: 7bc2d29bcd3a1d7810a621e611a1f9d61c173dd0
Parents: 9fefa0c
Author: ZuluPro <[email protected]>
Authored: Sun Jul 26 04:16:37 2015 -0400
Committer: Tomaz Muraus <[email protected]>
Committed: Sat Aug 15 16:32:41 2015 +0200

----------------------------------------------------------------------
 CHANGES.rst                                     |   5 +
 docs/compute/drivers/runabove.rst               |  15 +-
 docs/examples/compute/runabove/attach_volume.py |  11 +
 libcloud/compute/drivers/runabove.py            | 297 +++++++++++++++----
 .../compute/fixtures/runabove/volume_get.json   |   1 +
 .../fixtures/runabove/volume_get_detail.json    |   1 +
 libcloud/test/compute/test_runabove.py          |  53 ++++
 7 files changed, 317 insertions(+), 66 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/7bc2d29b/CHANGES.rst
----------------------------------------------------------------------
diff --git a/CHANGES.rst b/CHANGES.rst
index d3741b4..dc1c025 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -12,6 +12,11 @@ Compute
   (GITHUB-516)
   [Syed Mushtaq Ahmed]
 
+- Add volume management methods and other various improvements and fixes in the
+  RunAbove driver.
+  (GITHUB-561)
+  [ZuluPro]
+
 Changes with Apache Libcloud 0.18.0
 -----------------------------------
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7bc2d29b/docs/compute/drivers/runabove.rst
----------------------------------------------------------------------
diff --git a/docs/compute/drivers/runabove.rst 
b/docs/compute/drivers/runabove.rst
index adf1b9d..2b3c476 100644
--- a/docs/compute/drivers/runabove.rst
+++ b/docs/compute/drivers/runabove.rst
@@ -1,5 +1,5 @@
-Cloudwatt Compute Driver Documentation
-======================================
+RunAbove Compute Driver Documentation
+=====================================
 
 `RunAbove`_ is a public cloud offer created by OVH Group with datacenters
 in North America and Europe.
@@ -18,7 +18,7 @@ Instantiating a driver
 When you instantiate a driver you need to pass the following arguments to the
 driver constructor:
 
-* ``user_id`` - Application key
+* ``key`` - Application key
 * ``secret`` - Application secret
 * ``ex_consumer_key`` - Consumer key
 
@@ -55,11 +55,16 @@ Now you have and can use you credentials with Libcloud.
 Examples
 --------
 
-Create instance
-~~~~~~~~~~~~~~~
+Create node
+~~~~~~~~~~~
 
 .. literalinclude:: /examples/compute/runabove/create_node.py
 
+Create and attach a volume to a node
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. literalinclude:: /examples/compute/runabove/attach_volume.py
+
 API Docs
 --------
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7bc2d29b/docs/examples/compute/runabove/attach_volume.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/runabove/attach_volume.py 
b/docs/examples/compute/runabove/attach_volume.py
new file mode 100644
index 0000000..c4c0178
--- /dev/null
+++ b/docs/examples/compute/runabove/attach_volume.py
@@ -0,0 +1,11 @@
+from libcloud.compute.types import Provider
+from libcloud.compute.providers import get_driver
+
+RunAbove = get_driver(Provider.RUNABOVE)
+driver = RunAbove('yourAppKey', 'yourAppSecret', 'YourConsumerKey')
+
+location = [l for l in driver.list_locations() if l.id == 'SBG-1'][0]
+node = driver.list_nodes()[0]
+
+volume = driver.create_volume(size=10, location=location)
+driver.attach_volume(node=node, volume=volume)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7bc2d29b/libcloud/compute/drivers/runabove.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/runabove.py 
b/libcloud/compute/drivers/runabove.py
index a3ff95b..f5fd348 100644
--- a/libcloud/compute/drivers/runabove.py
+++ b/libcloud/compute/drivers/runabove.py
@@ -12,11 +12,13 @@
 # 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.
-
+"""
+RunAbove driver
+"""
 from libcloud.common.runabove import API_ROOT, RunAboveConnection
 from libcloud.compute.base import NodeDriver, NodeSize, Node, NodeLocation
-from libcloud.compute.base import NodeImage
-from libcloud.compute.types import Provider
+from libcloud.compute.base import NodeImage, StorageVolume
+from libcloud.compute.types import Provider, StorageVolumeState
 from libcloud.compute.drivers.openstack import OpenStackNodeDriver
 from libcloud.compute.drivers.openstack import OpenStackKeyPair
 
@@ -25,24 +27,9 @@ class RunAboveNodeDriver(NodeDriver):
     """
     Libcloud driver for the RunAbove API
 
-    Rough mapping of which is which:
-
-        list_nodes              linode.list
-        reboot_node             linode.reboot
-        destroy_node            linode.delete
-        create_node             linode.create, linode.update,
-                                linode.disk.createfromdistribution,
-                                linode.disk.create, linode.config.create,
-                                linode.ip.addprivate, linode.boot
-        list_sizes              avail.linodeplans
-        list_images             avail.distributions
-        list_locations          avail.datacenters
-        list_volumes            linode.disk.list
-        destroy_volume          linode.disk.delete
-
-    For more information on the Linode API, be sure to read the reference:
+    For more information on the RunAbove API, read the official reference:
 
-        http://www.linode.com/api/
+        https://api.runabove.com/console/
     """
     type = Provider.RUNABOVE
     name = "RunAbove"
@@ -52,13 +39,20 @@ class RunAboveNodeDriver(NodeDriver):
     api_name = 'runabove'
 
     NODE_STATE_MAP = OpenStackNodeDriver.NODE_STATE_MAP
+    VOLUME_STATE_MAP = OpenStackNodeDriver.VOLUME_STATE_MAP
 
     def __init__(self, key, secret, ex_consumer_key=None):
         """
-        Instantiate the driver with the given API key
+        Instantiate the driver with the given API credentials.
 
-        :param   key: the API key to use (required)
-        :type    key: ``str``
+        :param key: Your application key (required)
+        :type key: ``str``
+
+        :param secret: Your application secret (required)
+        :type secret: ``str``
+
+        :param ex_consumer_key: Your consumer key (required)
+        :type ex_consumer_key: ``str``
 
         :rtype: ``None``
         """
@@ -68,14 +62,12 @@ class RunAboveNodeDriver(NodeDriver):
 
     def list_nodes(self, location=None):
         """
-        List all Linodes that the API key can access
+        List all nodes.
 
-        This call will return all Linodes that the API key in use has access
-        to.
-        If a node is in this list, rebooting will work; however, creation and
-        destruction are a separate grant.
+        :keyword location: Location (region) used as filter
+        :type    location: :class:`NodeLocation`
 
-        :return: List of node objects that the API key can access
+        :return: List of node objects
         :rtype: ``list`` of :class:`Node`
         """
         action = API_ROOT + '/instance'
@@ -86,20 +78,50 @@ class RunAboveNodeDriver(NodeDriver):
         return self._to_nodes(response.object)
 
     def ex_get_node(self, node_id):
+        """
+        Get a individual node.
+
+        :keyword node_id: Node's ID
+        :type    node_id: ``str``
+
+        :return: Created node
+        :rtype  : :class:`Node`
+        """
         action = API_ROOT + '/instance/' + node_id
         response = self.connection.request(action, method='GET')
         return self._to_node(response.object)
 
-    def create_node(self, **kwargs):
+    def create_node(self, name, image, size, location, ex_keyname=None):
+        """
+        Create a new node
+
+        :keyword name: Name of created node
+        :type    name: ``str``
+
+        :keyword image: Image used for node
+        :type    image: :class:`NodeImage`
+
+        :keyword size: Size (flavor) used for node
+        :type    size: :class:`NodeSize`
+
+        :keyword location: Location (region) where to create node
+        :type    location: :class:`NodeLocation`
+
+        :keyword ex_keyname: Name of SSH key used
+        :type    ex_keyname: ``str``
+
+        :retunrs: Created node
+        :rtype  : :class:`Node`
+        """
         action = API_ROOT + '/instance'
         data = {
-            'name': kwargs["name"],
-            'imageId': kwargs["image"].id,
-            'flavorId': kwargs["size"].id,
-            'region': kwargs["location"].id,
+            'name': name,
+            'imageId': image.id,
+            'flavorId': size.id,
+            'region': location.id,
         }
-        if kwargs.get('ex_keyname'):
-            data['sshKeyName'] = kwargs['ex_keyname']
+        if ex_keyname:
+            data['sshKeyName'] = ex_keyname
         response = self.connection.request(action, data=data, method='POST')
         return self._to_node(response.object)
 
@@ -109,14 +131,6 @@ class RunAboveNodeDriver(NodeDriver):
         return True
 
     def list_sizes(self, location=None):
-        """
-        List available RunAbove flavors.
-
-        :keyword location: the facility to retrieve plans in
-        :type    location: :class:`NodeLocation`
-
-        :rtype: ``list`` of :class:`NodeSize`
-        """
         action = API_ROOT + '/flavor'
         data = {}
         if location:
@@ -125,24 +139,38 @@ class RunAboveNodeDriver(NodeDriver):
         return self._to_sizes(response.object)
 
     def ex_get_size(self, size_id):
+        """
+        Get an individual size (flavor).
+
+        :keyword size_id: Size's ID
+        :type    size_id: ``str``
+
+        :return: Size
+        :rtype: :class:`NodeSize`
+        """
         action = API_ROOT + '/flavor/' + size_id
         response = self.connection.request(action)
         return self._to_size(response.object)
 
-    def list_images(self, location=None, size=None):
+    def list_images(self, location=None, ex_size=None):
         """
-        List available Linux distributions
+        List available images
+
+        :keyword location: Location (region) used as filter
+        :type    location: :class:`NodeLocation`
 
-        Retrieve all Linux distributions that can be deployed to a Linode.
+        :keyword ex_size: Exclude images which are uncompatible with given size
+        :type    ex_size: :class:`NodeImage`
 
-        :rtype: ``list`` of :class:`NodeImage`
+        :return: List of images
+        :rtype  : ``list`` of :class:`NodeImage`
         """
         action = API_ROOT + '/image'
         data = {}
         if location:
             data['region'] = location.id
-        if size:
-            data['flavorId'] = size.id
+        if ex_size:
+            data['flavorId'] = ex_size.id
         response = self.connection.request(action, data=data)
         return self._to_images(response.object)
 
@@ -152,18 +180,20 @@ class RunAboveNodeDriver(NodeDriver):
         return self._to_image(response.object)
 
     def list_locations(self):
-        """
-        List available facilities for deployment
-
-        Retrieve all facilities that a Linode can be deployed in.
-
-        :rtype: ``list`` of :class:`NodeLocation`
-        """
         action = API_ROOT + '/region'
         data = self.connection.request(action)
         return self._to_locations(data.object)
 
     def list_key_pairs(self, location=None):
+        """
+        List available SSH public keys.
+
+        :keyword location: Location (region) used as filter
+        :type    location: :class:`NodeLocation`
+
+        :return: Public keys
+        :rtype: ``list``of :class:`KeyPair`
+        """
         action = API_ROOT + '/ssh'
         data = {}
         if location:
@@ -172,6 +202,18 @@ class RunAboveNodeDriver(NodeDriver):
         return self._to_key_pairs(response.object)
 
     def get_key_pair(self, name, location):
+        """
+        Get an individual SSH public key by its name and location.
+
+        :keyword name: SSH key name
+        :type name: str
+
+        :keyword location: Key's region
+        :type location: :class:`NodeLocation`
+
+        :return: Public key
+        :rtype: :class:`KeyPair`
+        """
         action = API_ROOT + '/ssh/' + name
         data = {'region': location.id}
         response = self.connection.request(action, data=data)
@@ -179,7 +221,7 @@ class RunAboveNodeDriver(NodeDriver):
 
     def import_key_pair_from_string(self, name, key_material, location):
         """
-        Import a new public key.
+        Import a new public key from string.
 
         :param name: Key pair name.
         :type name: ``str``
@@ -188,7 +230,7 @@ class RunAboveNodeDriver(NodeDriver):
         :type key_material: ``str``
 
         :return: Imported key pair object.
-        :rtype: :class:`.KeyPair`
+        :rtype: :class:`KeyPair`
         """
         action = API_ROOT + '/ssh'
         data = {'name': name, 'publicKey': key_material, 'region': location.id}
@@ -199,8 +241,11 @@ class RunAboveNodeDriver(NodeDriver):
         """
         Delete an existing key pair.
 
-        :param key_pair: Key pair object or ID.
-        :type key_pair: :class.KeyPair` or ``int``
+        :param name: Key pair name.
+        :type name: ``str``
+
+        :keyword location: Key's region
+        :type location: :class:`NodeLocation`
 
         :return:   True of False based on success of Keypair deletion
         :rtype:    ``bool``
@@ -210,6 +255,136 @@ class RunAboveNodeDriver(NodeDriver):
         self.connection.request(action, data=data, method='DELETE')
         return True
 
+    def create_volume(self, size, location, name=None,
+                      ex_volume_type='classic', ex_description=None):
+        """
+        Create a volume.
+
+        :param size: Size of volume to create (in GB).
+        :type size: ``int``
+
+        :param name: Name of volume to create
+        :type name: ``str``
+
+        :keyword location: Location to create the volume in
+        :type location: :class:`NodeLocation` or ``None``
+
+        :keyword ex_volume_type: ``'classic'`` or ``'high-speed'``
+        :type ex_volume_type: ``str``
+
+        :keyword ex_description: Optionnal description of volume
+        :type ex_description: str
+
+        :return:  Storage Volume object
+        :rtype:   :class:`StorageVolume`
+        """
+        action = API_ROOT + '/volume'
+        data = {
+            'region': location.id,
+            'size': str(size),
+            'type': ex_volume_type,
+        }
+        if name:
+            data['name'] = name
+        if ex_description:
+            data['description'] = ex_description
+        response = self.connection.request(action, data=data, method='POST')
+        return self._to_volume(response.object)
+
+    def destroy_volume(self, volume):
+        action = API_ROOT + '/volume/' + volume.id
+        self.connection.request(action, method='DELETE')
+        return True
+
+    def list_volumes(self, location=None):
+        """
+        Return a list of volumes.
+
+        :keyword location: Location use for filter
+        :type location: :class:`NodeLocation` or ``None``
+
+        :return: A list of volume objects.
+        :rtype: ``list`` of :class:`StorageVolume`
+        """
+        action = API_ROOT + '/volume'
+        data = {}
+        if location:
+            data['region'] = location.id
+        response = self.connection.request(action, data=data)
+        return self._to_volumes(response.object)
+
+    def ex_get_volume(self, volume_id):
+        """
+        Return a Volume object based on a volume ID.
+
+        :param  volume_id: The ID of the volume
+        :type   volume_id: ``int``
+
+        :return:  A StorageVolume object for the volume
+        :rtype:   :class:`StorageVolume`
+        """
+        action = API_ROOT + '/volume/' + volume_id
+        response = self.connection.request(action)
+        return self._to_volume(response.object)
+
+    def attach_volume(self, node, volume, device=None):
+        """
+        Attach a volume to a node.
+
+        :param node: Node where to attach volume
+        :type node: :class:`Node`
+
+        :param volume: The ID of the volume
+        :type volume: :class:`StorageVolume`
+
+        :param device: Unsed parameter
+
+        :return: True or False representing operation successful
+        :rtype:   ``bool``
+        """
+        action = '%s/volume/%s/attach' % (API_ROOT, volume.id)
+        data = {'instanceId': node.id}
+        self.connection.request(action, data=data, method='POST')
+        return True
+
+    def detach_volume(self, volume, ex_node=None):
+        """
+        Detach a volume to a node.
+
+        :param volume: The ID of the volume
+        :type volume: :class:`StorageVolume`
+
+        :param ex_node: Node to detach from (optionnal if volume is attached
+                        to only one node)
+        :type ex_node: :class:`Node`
+
+        :return: True or False representing operation successful
+        :rtype:   ``bool``
+
+        :raises: Exception: If ``ex_node`` is not provided and more than one
+                            node is attached to the volume
+        """
+        action = '%s/volume/%s/detach' % (API_ROOT, volume.id)
+        if ex_node is None:
+            if len(volume.extra['attachedTo']) != 1:
+                err_msg = "Volume '%s' has more or less than one attached \
+                    nodes, you must specify one."
+                raise Exception(err_msg)
+            ex_node = self.ex_get_node(volume.extra['attachedTo'][0])
+        data = {'instanceId': ex_node.id}
+        self.connection.request(action, data=data, method='POST')
+        return True
+
+    def _to_volume(self, obj):
+        extra = obj.copy()
+        extra.pop('id')
+        extra.pop('name')
+        extra.pop('size')
+        state = self.VOLUME_STATE_MAP.get(obj.pop('status', None),
+                                          StorageVolumeState.UNKNOWN)
+        return StorageVolume(id=obj['id'], name=obj['name'], size=obj['size'],
+                             state=state, extra=extra, driver=self)
+
     def _to_volumes(self, objs):
         return [self._to_volume(obj) for obj in objs]
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7bc2d29b/libcloud/test/compute/fixtures/runabove/volume_get.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/runabove/volume_get.json 
b/libcloud/test/compute/fixtures/runabove/volume_get.json
new file mode 100644
index 0000000..aa0b360
--- /dev/null
+++ b/libcloud/test/compute/fixtures/runabove/volume_get.json
@@ -0,0 +1 @@
+[{"id": "foo", "attachedTo": [], "created": "2015-08-09T15:13:59.459187Z", 
"name": "testvol", "description": "", "size": 10, "status": "creating", 
"region": "SBG-1", "type": "classic" }]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7bc2d29b/libcloud/test/compute/fixtures/runabove/volume_get_detail.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/runabove/volume_get_detail.json 
b/libcloud/test/compute/fixtures/runabove/volume_get_detail.json
new file mode 100644
index 0000000..861cb4e
--- /dev/null
+++ b/libcloud/test/compute/fixtures/runabove/volume_get_detail.json
@@ -0,0 +1 @@
+{"id": "foo", "attachedTo": [], "created": "2015-08-09T15:13:59.459187Z", 
"name": "testvol", "description": "", "size": 10, "status": "creating", 
"region": "SBG-1", "type": "classic" }

http://git-wip-us.apache.org/repos/asf/libcloud/blob/7bc2d29b/libcloud/test/compute/test_runabove.py
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/test_runabove.py 
b/libcloud/test/compute/test_runabove.py
index 54e881e..a1538e0 100644
--- a/libcloud/test/compute/test_runabove.py
+++ b/libcloud/test/compute/test_runabove.py
@@ -80,6 +80,29 @@ class RunAboveMockHttp(BaseRunAboveMockHttp):
         body = self.fixtures.load('instance_get_detail.json')
         return (httplib.OK, body, {}, httplib.responses[httplib.OK])
 
+    def _json_1_0_volume_get(self, method, url, body, headers):
+        body = self.fixtures.load('volume_get.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _json_1_0_volume_post(self, method, url, body, headers):
+        body = self.fixtures.load('volume_get_detail.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _json_1_0_volume_foo_get(self, method, url, body, headers):
+        body = self.fixtures.load('volume_get_detail.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _json_1_0_volume_foo_delete(self, method, url, body, headers):
+        return (httplib.OK, '', {}, httplib.responses[httplib.OK])
+
+    def _json_1_0_volume_foo_attach_post(self, method, url, body, headers):
+        body = self.fixtures.load('volume_get_detail.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _json_1_0_volume_foo_detach_post(self, method, url, body, headers):
+        body = self.fixtures.load('volume_get_detail.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
 
 @patch('libcloud.common.runabove.RunAboveConnection._timedelta', 42)
 class RunAboveTests(unittest.TestCase):
@@ -144,5 +167,35 @@ class RunAboveTests(unittest.TestCase):
         node = self.driver.list_nodes()[0]
         self.driver.destroy_node(node)
 
+    def test_list_volumes(self):
+        volumes = self.driver.list_volumes()
+        self.assertTrue(len(volumes) > 0)
+
+    def test_get_volume(self):
+        volume = self.driver.ex_get_volume('foo')
+        self.assertEqual(volume.name, 'testvol')
+
+    def test_create_volume(self):
+        location = self.driver.list_locations()[0]
+        volume = self.driver.create_volume(size=10, name='testvol',
+                                           location=location)
+        self.assertEqual(volume.name, 'testvol')
+
+    def test_destroy_volume(self):
+        volume = self.driver.list_volumes()[0]
+        self.driver.destroy_volume(volume)
+
+    def test_attach_volume(self):
+        node = self.driver.list_nodes()[0]
+        volume = self.driver.ex_get_volume('foo')
+        response = self.driver.attach_volume(node=node, volume=volume)
+        self.assertTrue(response)
+
+    def test_detach_volume(self):
+        node = self.driver.list_nodes()[0]
+        volume = self.driver.ex_get_volume('foo')
+        response = self.driver.detach_volume(ex_node=node, volume=volume)
+        self.assertTrue(response)
+
 if __name__ == '__main__':
     sys.exit(unittest.main())

Reply via email to