Repository: libcloud
Updated Branches:
  refs/heads/trunk 75b129e8b -> b2662d529


An initial implementation for the cloudscale.ch API.


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

Branch: refs/heads/trunk
Commit: c6ad7015a7adb181b867c6b1e848262d2ba52cf3
Parents: fe34a54
Author: Dave Halter <davidhalte...@gmail.com>
Authored: Tue Nov 22 13:50:41 2016 +0100
Committer: Dave Halter <davidhalte...@gmail.com>
Committed: Tue Nov 22 13:50:41 2016 +0100

----------------------------------------------------------------------
 libcloud/compute/drivers/cloudscale.py          | 211 +++++++++++++++++++
 libcloud/compute/providers.py                   |   2 +
 libcloud/compute/types.py                       |   2 +
 .../fixtures/cloudscale/create_node.json        |  46 ++++
 .../fixtures/cloudscale/list_images.json        |   1 +
 .../compute/fixtures/cloudscale/list_nodes.json |  48 +++++
 .../compute/fixtures/cloudscale/list_sizes.json |   2 +
 libcloud/test/compute/test_cloudscale.py        | 122 +++++++++++
 libcloud/test/secrets.py-dist                   |   3 +-
 9 files changed, 436 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/c6ad7015/libcloud/compute/drivers/cloudscale.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/cloudscale.py 
b/libcloud/compute/drivers/cloudscale.py
new file mode 100644
index 0000000..a439b12
--- /dev/null
+++ b/libcloud/compute/drivers/cloudscale.py
@@ -0,0 +1,211 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+A driver for cloudscale.ch.
+"""
+
+import json
+
+from libcloud.utils.py3 import httplib
+
+from libcloud.common.base import ConnectionKey, JsonResponse
+from libcloud.compute.types import Provider, NodeState
+from libcloud.common.types import InvalidCredsError
+from libcloud.compute.base import NodeDriver
+from libcloud.compute.base import Node, NodeImage, NodeSize
+
+
+class CloudscaleResponse(JsonResponse):
+    valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED,
+                            httplib.NO_CONTENT]
+
+    def parse_error(self):
+        body = self.parse_body()
+        if self.status == httplib.UNAUTHORIZED:
+            raise InvalidCredsError(body['detail'])
+        else:
+            # We are taking the first issue here. There might be multiple ones,
+            # but that doesn't really matter. It's nicer if the error is just
+            # one error (because it's a Python API and there's only one
+            # exception.
+            return next(iter(body.values()))
+
+    def success(self):
+        return self.status in self.valid_response_codes
+
+
+class CloudscaleConnection(ConnectionKey):
+    """
+    Connection class for the Vultr driver.
+    """
+    host = 'api.cloudscale.ch'
+    responseCls = CloudscaleResponse
+
+    def add_default_headers(self, headers):
+        """
+        Add headers that are necessary for every request
+
+        This method adds ``token`` to the request.
+        """
+        headers['Authorization'] = 'Bearer %s' % (self.key)
+        headers['Content-Type'] = 'application/json'
+        return headers
+
+
+class CloudscaleNodeDriver(NodeDriver):
+    """
+    Cloudscale's node driver.
+    """
+
+    connectionCls = CloudscaleConnection
+
+    type = Provider.CLOUDSCALE
+    name = 'Cloudscale'
+    website = 'https://www.cloudscale.ch'
+
+    NODE_STATE_MAP = dict(
+        changing=NodeState.PENDING,
+        running=NodeState.RUNNING,
+        stopped=NodeState.STOPPED,
+        paused=NodeState.PAUSED,
+    )
+
+    def __init__(self, key, **kwargs):
+        super(CloudscaleNodeDriver, self).__init__(key, **kwargs)
+
+    def list_nodes(self):
+        return self._list_resources('/v1/servers', self._to_node)
+
+    def list_sizes(self):
+        return self._list_resources('/v1/flavors', self._to_size)
+
+    def list_images(self):
+        return self._list_resources('/v1/images', self._to_image)
+
+    def create_node(self, name, size, image, location=None, ex_create_attr={}):
+        """
+        Create a node.
+
+        The `ex_create_attr` parameter can include the following dictionary
+        key and value pairs:
+
+        * `ssh_keys`: ``list`` of ``str`` ssh public keys
+        * `volume_size_gb`: ``int`` defaults to 10.
+        * `bulk_volume_size_gb`: defaults to None.
+        * `use_public_network`: ``bool`` defaults to True
+        * `use_private_network`: ``bool`` defaults to False
+        * `use_ipv6`: ``bool`` defaults to True
+        * `anti_affinity_with`: ``uuid`` of a server to create an anti-affinity
+          group with that server or add it to the same group as that server.
+        * `user_data`: ``str`` for optional cloud-config data
+
+        :keyword ex_create_attr: A dictionary of optional attributes for
+                                 droplet creation
+        :type ex_create_attr: ``dict``
+
+        :keyword    ex_user_data:  User data to be added to the node on create.
+                                     (optional)
+        :type       ex_user_data:  ``str``
+
+        :return: The newly created node.
+        :rtype: :class:`Node`
+        """
+        attr = dict(ex_create_attr)
+        attr.update(
+            name=name,
+            image=image.id,
+            flavor=size.id,
+        )
+        result = self.connection.request(
+            '/v1/servers',
+            data=json.dumps(attr),
+            method='POST'
+        )
+        return self._to_node(result.object)
+
+    def reboot_node(self, node):
+        return self._action(node, 'reboot')
+
+    def ex_start_node(self, node):
+        return self._action(node, 'start')
+
+    def ex_stop_node(self, node):
+        return self._action(node, 'stop')
+
+    def ex_node_by_uuid(self, uuid):
+        res = self.connection.request(self._get_server_url(uuid))
+        return self._to_node(res.object)
+
+    def destroy_node(self, node):
+        res = self.connection.request(self._get_server_url(node.id), 
method='DELETE')
+        return res.status == httplib.NO_CONTENT
+
+    def _get_server_url(self, uuid):
+        return '/v1/servers/%s' % uuid
+
+    def _action(self, node, action_name):
+        response = self.connection.request(
+            self._get_server_url(node.id) + '/' + action_name,
+            method='POST'
+        )
+        return response.status == httplib.OK
+
+    def _list_resources(self, url, tranform_func):
+        data = self.connection.request(url, method='GET').object
+        return [tranform_func(obj) for obj in data]
+
+    def _to_node(self, data):
+        state = self.NODE_STATE_MAP.get(data['status'], NodeState.UNKNOWN)
+        extra_keys = ['volumes', 'inferfaces', 'anti_affinity_with']
+        extra = {}
+        for key in extra_keys:
+            if key in data:
+                extra[key] = data[key]
+
+        public_ips = []
+        private_ips = []
+        for interface in data['interfaces']:
+            if interface['type'] == 'public':
+                ips = public_ips
+            else:
+                ips = private_ips
+            for address_obj in interface['addresses']:
+                ips.append(address_obj['address'])
+
+        return Node(
+            id=data['uuid'],
+            name=data['name'],
+            state=state,
+            public_ips=public_ips,
+            private_ips=private_ips,
+            extra=extra,
+            driver=self,
+            image=self._to_image(data['image']),
+            size=self._to_size(data['flavor']),
+        )
+
+    def _to_size(self, data):
+        extra = {'vcpu_count': data['vcpu_count']}
+        ram = data['memory_gb'] * 1024
+
+        return NodeSize(id=data['slug'], name=data['name'],
+                        ram=ram, disk=10,
+                        bandwidth=0, price=0,
+                        extra=extra, driver=self)
+
+    def _to_image(self, data):
+        extra = {'operating_system': data['operating_system']}
+        return NodeImage(id=data['slug'], name=data['name'], extra=extra,
+                         driver=self)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c6ad7015/libcloud/compute/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/providers.py b/libcloud/compute/providers.py
index ccdebc8..2d5127e 100644
--- a/libcloud/compute/providers.py
+++ b/libcloud/compute/providers.py
@@ -141,6 +141,8 @@ DRIVERS = {
     ('libcloud.compute.drivers.ntta', 'NTTAmericaNodeDriver'),
     Provider.ALIYUN_ECS:
     ('libcloud.compute.drivers.ecs', 'ECSDriver'),
+    Provider.CLOUDSCALE:
+    ('libcloud.compute.drivers.cloudscale', 'CloudscaleNodeDriver'),
 }
 
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c6ad7015/libcloud/compute/types.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py
index 3b63b9e..658907e 100644
--- a/libcloud/compute/types.py
+++ b/libcloud/compute/types.py
@@ -70,6 +70,7 @@ class Provider(Type):
     :cvar AZURE_ARM: Azure Resource Manager (modern) driver.
     :cvar BLUEBOX: Bluebox
     :cvar CLOUDSIGMA: CloudSigma
+    :cvar CLOUDSCALE: cloudscale.ch
     :cvar CLOUDSTACK: CloudStack
     :cvar DIMENSIONDATA: Dimension Data Cloud
     :cvar EC2: Amazon AWS.
@@ -115,6 +116,7 @@ class Provider(Type):
     CISCOCCS = 'ciscoccs'
     CLOUDFRAMES = 'cloudframes'
     CLOUDSIGMA = 'cloudsigma'
+    CLOUDSCALE = 'cloudscale'
     CLOUDSTACK = 'cloudstack'
     CLOUDWATT = 'cloudwatt'
     DIGITAL_OCEAN = 'digitalocean'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c6ad7015/libcloud/test/compute/fixtures/cloudscale/create_node.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/cloudscale/create_node.json 
b/libcloud/test/compute/fixtures/cloudscale/create_node.json
new file mode 100644
index 0000000..a40e80c
--- /dev/null
+++ b/libcloud/test/compute/fixtures/cloudscale/create_node.json
@@ -0,0 +1,46 @@
+{
+  "href": 
"https://api.cloudscale.ch/v1/servers/47cec963-fcd2-482f-bdb6-24461b2d47b1";,
+  "uuid": "47cec963-fcd2-482f-bdb6-24461b2d47b1",
+  "name": "db-master",
+  "status": "changing",
+  "flavor": {
+    "slug": "flex-4",
+    "name": "Flex-4",
+    "vcpu_count": 2,
+    "memory_gb": 4
+  },
+  "image": {
+    "slug": "debian-8",
+    "name": "Debian 8 (2016-10-20)",
+    "operating_system": "Debian"
+  },
+  "volumes": [
+    {
+      "type": "ssd",
+      "device_path": "",
+      "size_gb": 50
+    }
+  ],
+  "interfaces": [
+    {
+      "type": "public",
+      "addresses": [
+        {
+          "version": 4,
+          "address": "185.98.122.176",
+          "prefix_length": 24,
+          "gateway": "185.98.122.1",
+          "reverse_ptr": "185-98-122-176.cust.cloudscale.ch"
+        },
+        {
+          "version": 6,
+          "address": "2a06:c01:1:1902::7ab0:176",
+          "prefix_length": 64,
+          "gateway": "fe80::1",
+          "reverse_ptr": "185-98-122-176.cust.cloudscale.ch"
+        }
+      ]
+    }
+  ],
+  "anti_affinity_with": []
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c6ad7015/libcloud/test/compute/fixtures/cloudscale/list_images.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/cloudscale/list_images.json 
b/libcloud/test/compute/fixtures/cloudscale/list_images.json
new file mode 100644
index 0000000..e8ed55c
--- /dev/null
+++ b/libcloud/test/compute/fixtures/cloudscale/list_images.json
@@ -0,0 +1 @@
+[{"slug":"ubuntu-16.10","name":"Ubuntu 16.10 (2016-10-17)","operating_system": 
"Ubuntu"}]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c6ad7015/libcloud/test/compute/fixtures/cloudscale/list_nodes.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/cloudscale/list_nodes.json 
b/libcloud/test/compute/fixtures/cloudscale/list_nodes.json
new file mode 100644
index 0000000..95f861c
--- /dev/null
+++ b/libcloud/test/compute/fixtures/cloudscale/list_nodes.json
@@ -0,0 +1,48 @@
+[
+  {
+    "href": 
"https://api.cloudscale.ch/v1/servers/47cec963-fcd2-482f-bdb6-24461b2d47b1";,
+    "uuid": "47cec963-fcd2-482f-bdb6-24461b2d47b1",
+    "name": "db-master",
+    "status": "running",
+    "flavor": {
+      "slug": "flex-4",
+      "name": "Flex-4",
+      "vcpu_count": 2,
+      "memory_gb": 4
+    },
+    "image": {
+      "slug": "debian-8",
+      "name": "Debian 8 (2016-10-20)",
+      "operating_system": "Debian"
+    },
+    "volumes": [
+      {
+        "type": "ssd",
+        "device_path": "/dev/vda",
+        "size_gb": 50
+      }
+    ],
+    "interfaces": [
+      {
+        "type": "public",
+        "addresses": [
+          {
+            "version": 4,
+            "address": "185.98.122.176",
+            "prefix_length": 24,
+            "gateway": "185.98.122.1",
+            "reverse_ptr": "185-98-122-176.cust.cloudscale.ch"
+          },
+          {
+            "version": 6,
+            "address": "2a06:c01:1:1902::7ab0:176",
+            "prefix_length": 64,
+            "gateway": "fe80::1",
+            "reverse_ptr": "185-98-122-176.cust.cloudscale.ch"
+          }
+        ]
+      }
+    ],
+    "anti_affinity_with": []
+  }
+]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c6ad7015/libcloud/test/compute/fixtures/cloudscale/list_sizes.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/cloudscale/list_sizes.json 
b/libcloud/test/compute/fixtures/cloudscale/list_sizes.json
new file mode 100644
index 0000000..8952a5e
--- /dev/null
+++ b/libcloud/test/compute/fixtures/cloudscale/list_sizes.json
@@ -0,0 +1,2 @@
+[{"slug":"flex-2","name":"Flex-2","vcpu_count":1,"memory_gb":2},
+ {"slug":"flex-4","name":"Flex-4","vcpu_count":2,"memory_gb": 4}]

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c6ad7015/libcloud/test/compute/test_cloudscale.py
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/test_cloudscale.py 
b/libcloud/test/compute/test_cloudscale.py
new file mode 100644
index 0000000..fc1cabb
--- /dev/null
+++ b/libcloud/test/compute/test_cloudscale.py
@@ -0,0 +1,122 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import sys
+import unittest
+
+try:
+    import simplejson as json
+except ImportError:
+    import json  # NOQA
+
+from libcloud.utils.py3 import httplib
+
+from libcloud.compute.drivers.cloudscale import CloudscaleNodeDriver
+
+from libcloud.test import LibcloudTestCase, MockHttpTestCase
+from libcloud.test.file_fixtures import ComputeFileFixtures
+from libcloud.test.secrets import CLOUDSCALE_PARAMS
+
+
+class CloudscaleTests(LibcloudTestCase):
+
+    def setUp(self):
+        CloudscaleNodeDriver.connectionCls.conn_classes = \
+            (None, CloudscaleMockHttp)
+        self.driver = CloudscaleNodeDriver(*CLOUDSCALE_PARAMS)
+
+    def test_list_images_success(self):
+        images = self.driver.list_images()
+        image, = images
+        self.assertTrue(image.id is not None)
+        self.assertTrue(image.name is not None)
+
+    def test_list_sizes_success(self):
+        sizes = self.driver.list_sizes()
+        self.assertEqual(len(sizes), 2)
+
+        size = sizes[0]
+        self.assertTrue(size.id is not None)
+        self.assertEqual(size.name, 'Flex-2')
+        self.assertEqual(size.ram, 2048)
+
+        size = sizes[1]
+        self.assertTrue(size.id is not None)
+        self.assertEqual(size.name, 'Flex-4')
+        self.assertEqual(size.ram, 4096)
+
+    def test_list_locations_not_existing(self):
+        # assertRaises doesn't exist in Python 2.5?!
+        try:
+            self.driver.list_locations()
+        except NotImplementedError:
+            pass
+        else:
+            assert False, 'Did not raise the wished error.'
+
+    def test_list_nodes_success(self):
+        nodes = self.driver.list_nodes()
+        self.assertEqual(len(nodes), 1)
+        self.assertEqual(nodes[0].id, '47cec963-fcd2-482f-bdb6-24461b2d47b1')
+        self.assertEqual(
+            nodes[0].public_ips,
+            ['185.98.122.176', '2a06:c01:1:1902::7ab0:176']
+        )
+
+    def test_reboot_node_success(self):
+        node = self.driver.list_nodes()[0]
+        result = self.driver.reboot_node(node)
+        self.assertTrue(result)
+
+    def test_create_node_success(self):
+        test_size = self.driver.list_sizes()[0]
+        test_image = self.driver.list_images()[0]
+        created_node = self.driver.create_node('node-name', test_size, 
test_image)
+        self.assertEqual(created_node.id, 
"47cec963-fcd2-482f-bdb6-24461b2d47b1")
+
+    def test_destroy_node_success(self):
+        node = self.driver.list_nodes()[0]
+        result = self.driver.destroy_node(node)
+        self.assertTrue(result)
+
+
+class CloudscaleMockHttp(MockHttpTestCase):
+    fixtures = ComputeFileFixtures('cloudscale')
+
+    def _v1_images(self, method, url, body, headers):
+        body = self.fixtures.load('list_images.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _v1_flavors(self, method, url, body, headers):
+        body = self.fixtures.load('list_sizes.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _v1_servers(self, method, url, body, headers):
+        if method == 'GET':
+            body = self.fixtures.load('list_nodes.json')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+        else:
+            body = self.fixtures.load('create_node.json')
+            response = httplib.responses[httplib.CREATED]
+            return (httplib.CREATED, body, {}, response)
+
+    def _v1_servers_47cec963_fcd2_482f_bdb6_24461b2d47b1(self, method, url, 
body, headers):
+        assert method == 'DELETE'
+        return (httplib.NO_CONTENT, "", {}, 
httplib.responses[httplib.NO_CONTENT])
+
+    def _v1_servers_47cec963_fcd2_482f_bdb6_24461b2d47b1_reboot(self, method, 
url, body, headers):
+        return (httplib.OK, "", {}, httplib.responses[httplib.OK])
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/c6ad7015/libcloud/test/secrets.py-dist
----------------------------------------------------------------------
diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist
index 8c73f54..2c21f84 100644
--- a/libcloud/test/secrets.py-dist
+++ b/libcloud/test/secrets.py-dist
@@ -51,6 +51,7 @@ PROFIT_BRICKS_PARAMS = ('user', 'key')
 VULTR_PARAMS = ('key')
 PACKET_PARAMS = ('api_key')
 ECS_PARAMS = ('access_key', 'access_secret')
+CLOUDSCALE_PARAMS = ('token',)
 
 # Storage
 STORAGE_S3_PARAMS = ('key', 'secret')
@@ -94,4 +95,4 @@ DNS_PARAMS_DNSPOD = ('key', )
 CONTAINER_PARAMS_DOCKER = ('user', 'password')
 CONTAINER_PARAMS_ECS = ('user', 'password', 'region')
 CONTAINER_PARAMS_KUBERNETES = ('user', 'password')
-CONTAINER_PARAMS_RANCHER = ('user', 'password')
\ No newline at end of file
+CONTAINER_PARAMS_RANCHER = ('user', 'password')

Reply via email to