libcloud-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From quent...@apache.org
Subject [1/4] libcloud git commit: Scaleway Compute Driver (squashed)
Date Mon, 11 Jun 2018 19:03:18 GMT
Repository: libcloud
Updated Branches:
  refs/heads/trunk c58e89222 -> 6de9bb4cf


Scaleway Compute Driver (squashed)

@bonifaido
- Scaleway Compute Driver
- Replace Scaleway logo
- Remove double slashes
- Fix private_ips access
@danhunsaker
- [scaleway] Update sizes; all location support
- [scaleway] Unify NodeImage handling
- [scaleway] More metadata
- [scaleway] Add SSH KeyPair Support
- [scaleway] Automatically handle minimum sizes
- [scaleway] Add Tests
- [scaleway] Lint Fixes
@sieben
- Remove useless parenthesis and add more specific exception
- Add an example for Scaleway
@danhunsaker
- [scaleway] Add docs
- [scaleway] Add pagination support
- [scaleway] Switch to API instead of hard-coded sizes
  The Scaleway API provides size info after all. Let's make use of it!
  Also, fix some of the docs...
- [scaleway] Fix size calcs
  And alert when trying to create a node with too much storage for the given server size. Base that calculation on the actual image size, rather than a hard-coded default.

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

Branch: refs/heads/trunk
Commit: 3315e9769403da9a26c27ddca1123b0432d958c4
Parents: c58e892
Author: Daniel Hunsaker <dan@nanobox.io>
Authored: Mon May 21 13:11:26 2018 -0600
Committer: Daniel Hunsaker <dan@nanobox.io>
Committed: Tue Jun 5 10:06:03 2018 -0600

----------------------------------------------------------------------
 docs/_static/images/provider_logos/scaleway.png | Bin 0 -> 11527 bytes
 docs/compute/drivers/scaleway.rst               |  30 +
 docs/examples/compute/scaleway/create_node.py   |  16 +
 docs/examples/compute/scaleway/list_nodes.py    |   9 +
 docs/examples/compute/scaleway/list_volumes.py  |  12 +
 libcloud/compute/drivers/scaleway.py            | 665 +++++++++++++++++++
 libcloud/compute/providers.py                   |   2 +
 libcloud/compute/types.py                       |   1 +
 .../compute/fixtures/scaleway/create_image.json |  21 +
 .../compute/fixtures/scaleway/create_node.json  |  40 ++
 .../fixtures/scaleway/create_volume.json        |  13 +
 .../scaleway/create_volume_snapshot.json        |  15 +
 .../test/compute/fixtures/scaleway/error.json   |   1 +
 .../fixtures/scaleway/error_invalid_image.json  |   1 +
 .../compute/fixtures/scaleway/get_image.json    |  21 +
 .../fixtures/scaleway/list_availability.json    |  13 +
 .../compute/fixtures/scaleway/list_images.json  |  42 ++
 .../compute/fixtures/scaleway/list_nodes.json   |  74 +++
 .../fixtures/scaleway/list_nodes_empty.json     |   3 +
 .../compute/fixtures/scaleway/list_sizes.json   |  76 +++
 .../scaleway/list_volume_snapshots.json         |  30 +
 .../compute/fixtures/scaleway/list_volumes.json |  26 +
 .../fixtures/scaleway/list_volumes_empty.json   |   3 +
 .../compute/fixtures/scaleway/reboot_node.json  |   9 +
 .../compute/fixtures/scaleway/token_info.json   |  14 +
 .../compute/fixtures/scaleway/user_info.json    |  15 +
 libcloud/test/compute/test_scaleway.py          | 334 ++++++++++
 libcloud/test/secrets.py-dist                   |   5 +-
 28 files changed, 1489 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/docs/_static/images/provider_logos/scaleway.png
----------------------------------------------------------------------
diff --git a/docs/_static/images/provider_logos/scaleway.png b/docs/_static/images/provider_logos/scaleway.png
new file mode 100644
index 0000000..2a2b9e0
Binary files /dev/null and b/docs/_static/images/provider_logos/scaleway.png differ

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/docs/compute/drivers/scaleway.rst
----------------------------------------------------------------------
diff --git a/docs/compute/drivers/scaleway.rst b/docs/compute/drivers/scaleway.rst
new file mode 100644
index 0000000..0afffec
--- /dev/null
+++ b/docs/compute/drivers/scaleway.rst
@@ -0,0 +1,30 @@
+Scaleway Compute Driver Documentation
+=====================================
+
+`Scaleway`_ is a dedicated bare metal cloud hosting provider based in Paris
+
+.. figure:: /_static/images/provider_logos/scaleway.png
+    :align: center
+    :width: 300
+    :target: https://www.scaleway.com/
+
+Instantiating a driver and listing nodes
+----------------------------------------
+
+.. literalinclude:: /examples/compute/scaleway/list_nodes.py
+   :language: python
+
+Instantiating a driver and listing volumes
+------------------------------------------
+
+.. literalinclude:: /examples/compute/scaleway/list_volumes.py
+  :language: python
+
+API Docs
+--------
+
+.. autoclass:: libcloud.compute.drivers.scaleway.ScalewayNodeDriver
+    :members:
+    :inherited-members:
+
+.. _`Scaleway`: https://www.scaleway.com/

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/docs/examples/compute/scaleway/create_node.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/scaleway/create_node.py b/docs/examples/compute/scaleway/create_node.py
new file mode 100644
index 0000000..bd1f81a
--- /dev/null
+++ b/docs/examples/compute/scaleway/create_node.py
@@ -0,0 +1,16 @@
+import os
+
+from libcloud.compute.drivers.scaleway import ScalewayNodeDriver
+
+driver = ScalewayNodeDriver(key=os.environ["SCW_ACCESS_KEY"],
+                            secret=os.environ["SCW_TOKEN"])
+
+images = [x for x in driver.list_images(region="par1")
+          if x.id == "89457135-d446-41ba-a8df-d53e5bb54710"]
+sizes = [x for x in driver.list_sizes() if x.name == "C2S"]
+
+# We create the node
+driver.create_node("foobar", size=sizes[0], image=images[0], region="par1")
+
+# We delete it right after
+driver.destroy_node("foobar")

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/docs/examples/compute/scaleway/list_nodes.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/scaleway/list_nodes.py b/docs/examples/compute/scaleway/list_nodes.py
new file mode 100644
index 0000000..8449259
--- /dev/null
+++ b/docs/examples/compute/scaleway/list_nodes.py
@@ -0,0 +1,9 @@
+from libcloud.compute.types import Provider
+from libcloud.compute.providers import get_driver
+
+cls = get_driver(Provider.SCALEWAY)
+driver = cls('SCALEWAY_ACCESS_KEY', 'SCALEWAY_SECRET_TOKEN')
+
+nodes = driver.list_nodes()
+for node in nodes:
+    print(node)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/docs/examples/compute/scaleway/list_volumes.py
----------------------------------------------------------------------
diff --git a/docs/examples/compute/scaleway/list_volumes.py b/docs/examples/compute/scaleway/list_volumes.py
new file mode 100644
index 0000000..931b496
--- /dev/null
+++ b/docs/examples/compute/scaleway/list_volumes.py
@@ -0,0 +1,12 @@
+from libcloud.compute.types import Provider
+from libcloud.compute.providers import get_driver
+
+cls = get_driver(Provider.SCALEWAY)
+driver = cls('SCALEWAY_ACCESS_KEY', 'SCALEWAY_SECRET_TOKEN')
+
+volumes = driver.list_volumes()
+for volume in volumes:
+    print(volume)
+    snapshots = driver.list_volume_snapshots(volume)
+    for snapshot in snapshots:
+        print("  snapshot-%s" % snapshot)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/compute/drivers/scaleway.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/scaleway.py b/libcloud/compute/drivers/scaleway.py
new file mode 100644
index 0000000..bbe726d
--- /dev/null
+++ b/libcloud/compute/drivers/scaleway.py
@@ -0,0 +1,665 @@
+# 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.
+"""
+Scaleway Driver
+"""
+
+import copy
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+from libcloud.common.base import ConnectionUserAndKey, JsonResponse
+from libcloud.common.types import ProviderError
+from libcloud.compute.base import NodeDriver, NodeImage, Node, NodeSize
+from libcloud.compute.base import NodeLocation
+from libcloud.compute.base import StorageVolume, VolumeSnapshot, KeyPair
+from libcloud.compute.providers import Provider
+from libcloud.compute.types import NodeState, VolumeSnapshotState
+from libcloud.utils.iso8601 import parse_date
+from libcloud.utils.py3 import httplib
+
+__all__ = [
+    'ScalewayResponse',
+    'ScalewayConnection',
+    'ScalewayNodeDriver'
+]
+
+SCALEWAY_API_HOSTS = {
+    'default': 'api.scaleway.com',
+    'account': 'account.scaleway.com',
+    'par1': 'cp-par1.scaleway.com',
+    'ams1': 'cp-ams1.scaleway.com',
+}
+
+# The API doesn't give location info, so we provide it ourselves, instead.
+SCALEWAY_LOCATION_DATA = [
+    {'id': 'par1', 'name': 'Paris 1', 'country': 'FR'},
+    {'id': 'ams1', 'name': 'Amsterdam 1', 'country': 'NL'},
+]
+
+
+class ScalewayResponse(JsonResponse):
+    valid_response_codes = [httplib.OK, httplib.ACCEPTED,
+                            httplib.CREATED, httplib.NO_CONTENT]
+
+    def parse_body(self):
+        body = super(ScalewayResponse, self).parse_body()
+
+        links = self.connection.connection.getresponse().links
+        if links and 'next' in links:
+            response = self.connection.request(links['next']['url'],
+                                               data=self.connection.data,
+                                               method=self.connection.method)
+            next = response.object
+            merged = {root: child + next[root]
+                      for root, child in list(body.items())}
+            body = merged
+
+        return body
+
+    def parse_error(self):
+        return super(ScalewayResponse, self).parse_error()['message']
+
+    def success(self):
+        return self.status in self.valid_response_codes
+
+
+class ScalewayConnection(ConnectionUserAndKey):
+    """
+    Connection class for the Scaleway driver.
+    """
+
+    host = SCALEWAY_API_HOSTS['default']
+    allow_insecure = False
+    responseCls = ScalewayResponse
+
+    def request(self, action, params=None, data=None, headers=None,
+                method='GET', raw=False, stream=False, region=None,
+                paged=False):
+        if region:
+            old_host = self.host
+            self.host = SCALEWAY_API_HOSTS[region.id
+                                           if isinstance(region, NodeLocation)
+                                           else region]
+            if not self.host == old_host:
+                self.connect()
+
+        if paged:
+            if params is None:
+                params = {}
+
+            if isinstance(params, dict):
+                params['per_page'] = 100
+            else:
+                params.append(('per_page', 100))
+
+        return super(ScalewayConnection, self).request(action, params, data,
+                                                       headers, method, raw,
+                                                       stream)
+
+    def add_default_headers(self, headers):
+        """
+        Add headers that are necessary for every request
+        """
+        headers['X-Auth-Token'] = self.key
+        headers['Content-Type'] = 'application/json'
+        return headers
+
+
+def _to_lib_size(size):
+    return int(size / 1000 / 1000 / 1000)
+
+
+def _to_api_size(size):
+    return int(size * 1000 * 1000 * 1000)
+
+
+class ScalewayNodeDriver(NodeDriver):
+    """
+    Scaleway Node Driver Class
+
+    This is the primary driver for interacting with Scaleway.  It contains all
+    of the standard libcloud methods that Scaleway's API supports.
+    """
+
+    type = Provider.SCALEWAY
+    connectionCls = ScalewayConnection
+    name = 'Scaleway'
+    website = 'https://www.scaleway.com/'
+
+    SNAPSHOT_STATE_MAP = {  # TODO map all states
+        'snapshotting': VolumeSnapshotState.CREATING
+    }
+
+    def list_locations(self):
+        """
+        List data centers available.
+
+        :return: list of node location objects
+        :rtype: ``list`` of :class:`.NodeLocation`
+        """
+        return [NodeLocation(driver=self, **copy.deepcopy(location))
+                for location in SCALEWAY_LOCATION_DATA]
+
+    def list_sizes(self, region=None):
+        """
+        List available VM sizes.
+
+        :param region: The region in which to list sizes
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: list of node size objects
+        :rtype: ``list`` of :class:`.NodeSize`
+        """
+        response = self.connection.request('/products/servers', region=region,
+                                           paged=True)
+        sizes = response.object['servers']
+
+        response = self.connection.request('/products/servers/availability',
+                                           region=region, paged=True)
+        availability = response.object['servers']
+
+        return sorted([self._to_size(name, sizes[name], availability[name])
+                       for name in sizes], key=lambda x: x.name)
+
+    def _to_size(self, name, size, availability):
+        min_disk = (_to_lib_size(size['volumes_constraint']['min_size'] or 0)
+                    if size['volumes_constraint'] else 25)
+        max_disk = (_to_lib_size(size['volumes_constraint']['max_size'] or 0)
+                    if size['volumes_constraint'] else min_disk)
+
+        extra = {
+            'cores': size['ncpus'],
+            'monthly': size['monthly_price'],
+            'arch': size['arch'],
+            'baremetal': size['baremetal'],
+            'availability': availability['availability'],
+            'max_disk': max_disk,
+            'internal_bandwidth': int(
+                (size['network']['sum_internal_bandwidth'] or 0) /
+                (1024 * 1024)),
+            'ipv6': size['network']['ipv6_support'],
+            'alt_names': size['alt_names'],
+        }
+
+        return NodeSize(id=name,
+                        name=name,
+                        ram=int((size['ram'] or 0) / (1024 * 1024)),
+                        disk=min_disk,
+                        bandwidth=int(
+                            (size['network']['sum_internet_bandwidth'] or 0) /
+                            (1024 * 1024)),
+                        price=size['hourly_price'],
+                        driver=self,
+                        extra=extra)
+
+    def list_images(self, region=None):
+        """
+        List available VM images.
+
+        :param region: The region in which to list images
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: list of image objects
+        :rtype: ``list`` of :class:`.NodeImage`
+        """
+        response = self.connection.request('/images', region=region,
+                                           paged=True)
+        images = response.object['images']
+        return [self._to_image(image) for image in images]
+
+    def create_image(self, node, name, region=None):
+        """
+        Create a VM image from an existing node's root volume.
+
+        :param node: The node from which to create the image
+        :type node: :class:`.Node`
+
+        :param name: The name to give the image
+        :type name: ``str``
+
+        :param region: The region in which to create the image
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: the newly created image object
+        :rtype: :class:`.NodeImage`
+        """
+        data = {
+            'organization': self.key,
+            'name': name,
+            'arch': node.extra['arch'],
+            'root_volume': node.extra['volumes']['0']['id']  # TODO check this
+        }
+        response = self.connection.request('/images', data=json.dumps(data),
+                                           region=region,
+                                           method='POST')
+        image = response.object['image']
+        return self._to_image(image)
+
+    def delete_image(self, node_image, region=None):
+        """
+        Delete a VM image.
+
+        :param node_image: The image to delete
+        :type node_image: :class:`.NodeImage`
+
+        :param region: The region in which to find/delete the image
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: True if the image was deleted, otherwise False
+        :rtype: ``bool``
+        """
+        return self.connection.request('/images/%s' % node_image.id,
+                                       region=region,
+                                       method='DELETE').success()
+
+    def get_image(self, image_id, region=None):
+        """
+        Retrieve a specific VM image.
+
+        :param image_id: The id of the image to retrieve
+        :type image_id: ``int``
+
+        :param region: The region in which to create the image
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: the requested image object
+        :rtype: :class:`.NodeImage`
+        """
+        response = self.connection.request('/images/%s' % image_id,
+                                           region=region)
+        image = response.object['image']
+        return self._to_image(image)
+
+    def _to_image(self, image):
+        extra = {
+            'arch': image['arch'],
+            'size': _to_lib_size(image.get('root_volume', {})
+                                      .get('size', 0)) or 50,
+            'creation_date': parse_date(image['creation_date']),
+            'modification_date': parse_date(image['modification_date']),
+            'organization': image['organization'],
+        }
+        return NodeImage(id=image['id'],
+                         name=image['name'],
+                         driver=self,
+                         extra=extra)
+
+    def list_nodes(self, region=None):
+        """
+        List all nodes.
+
+        :param region: The region in which to look for nodes
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: list of node objects
+        :rtype: ``list`` of :class:`.Node`
+        """
+        response = self.connection.request('/servers', region=region,
+                                           paged=True)
+        servers = response.object['servers']
+        return [self._to_node(server) for server in servers]
+
+    def _to_node(self, server):
+        public_ip = server['public_ip']
+        private_ip = server['private_ip']
+        location = server['location'] or {}
+        return Node(id=server['id'],
+                    name=server['name'],
+                    state=NodeState.fromstring(server['state']),
+                    public_ips=[public_ip['address']] if public_ip else [],
+                    private_ips=[private_ip] if private_ip else [],
+                    driver=self,
+                    extra={'volumes': server['volumes'],
+                           'tags': server['tags'],
+                           'arch': server['arch'],
+                           'organization': server['organization'],
+                           'region': location.get('zone_id', 'par1')},
+                    created_at=parse_date(server['creation_date']))
+
+    def create_node(self, name, size, image, ex_volumes=None, ex_tags=None,
+                    region=None):
+        """
+        Create a new node.
+
+        :param name: The name to give the node
+        :type name: ``str``
+
+        :param size: The size of node to create
+        :type size: :class:`.NodeSize`
+
+        :param image: The image to create the node with
+        :type image: :class:`.NodeImage`
+
+        :param ex_volumes: Additional volumes to create the node with
+        :type ex_volumes: ``dict`` of :class:`.StorageVolume`s
+
+        :param ex_tags: Tags to assign to the node
+        :type ex_tags: ``list`` of ``str``
+
+        :param region: The region in which to create the node
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: the newly created node object
+        :rtype: :class:`.Node`
+        """
+        data = {
+            'name': name,
+            'organization': self.key,
+            'image': image.id,
+            'volumes': ex_volumes or {},
+            'commercial_type': size.id,
+            'tags': ex_tags or []
+        }
+
+        allocate_space = image.extra.get('size', 50)
+        for volume in data['volumes']:
+            allocate_space += _to_lib_size(volume['size'])
+
+        while allocate_space < size.disk:
+            if size.disk - allocate_space > 150:
+                bump = 150
+            else:
+                bump = size.disk - allocate_space
+
+            vol_num = len(data['volumes']) + 1
+            data['volumes'][str(vol_num)] = {
+                "name": "%s-%d" % (name, vol_num),
+                "organization": self.key,
+                "size": _to_api_size(bump),
+                "volume_type": "l_ssd"
+            }
+            allocate_space += bump
+
+        if allocate_space > size.extra.get('max_disk', size.disk):
+            range = ("of %dGB" % size.disk
+                     if size.extra.get('max_disk', size.disk) == size.disk else
+                     "between %dGB and %dGB" %
+                     (size.extra.get('max_disk', size.disk), size.disk))
+            raise ProviderError(
+                value=("%s only supports a total volume size %s; tried %dGB" %
+                       (size.id, range, allocate_space)),
+                http_code=400, driver=self)
+
+        response = self.connection.request('/servers', data=json.dumps(data),
+                                           region=region, method='POST')
+        server = response.object['server']
+        node = self._to_node(server)
+        node.extra['region'] = (region.id if isinstance(region, NodeLocation)
+                                else region) or 'par1'
+
+        # Scaleway doesn't start servers by default, let's do it
+        self._action(node.id, 'poweron')
+
+        return node
+
+    def _action(self, server_id, action, region=None):
+        return self.connection.request('/servers/%s/action' % server_id,
+                                       region=region,
+                                       data=json.dumps({'action': action}),
+                                       method='POST').success()
+
+    def reboot_node(self, node):
+        """
+        Reboot a node.
+
+        :param node: The node to be rebooted
+        :type node: :class:`Node`
+
+        :return: True if the reboot was successful, otherwise False
+        :rtype: ``bool``
+        """
+        return self._action(node.id, 'reboot')
+
+    def destroy_node(self, node):
+        """
+        Destroy a node.
+
+        :param node: The node to be destroyed
+        :type node: :class:`Node`
+
+        :return: True if the destroy was successful, otherwise False
+        :rtype: ``bool``
+        """
+        return self._action(node.id, 'terminate')
+
+    def list_volumes(self, region=None):
+        """
+        Return a list of volumes.
+
+        :param region: The region in which to look for volumes
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: A list of volume objects.
+        :rtype: ``list`` of :class:`StorageVolume`
+        """
+        response = self.connection.request('/volumes', region=region,
+                                           paged=True)
+        volumes = response.object['volumes']
+        return [self._to_volume(volume) for volume in volumes]
+
+    def _to_volume(self, volume):
+        extra = {
+            'organization': volume['organization'],
+            'volume_type': volume['volume_type'],
+            'creation_date': parse_date(volume['creation_date']),
+            'modification_date': parse_date(volume['modification_date']),
+        }
+        return StorageVolume(id=volume['id'],
+                             name=volume['name'],
+                             size=_to_lib_size(volume['size']),
+                             driver=self,
+                             extra=extra)
+
+    def list_volume_snapshots(self, volume, region=None):
+        """
+        List snapshots for a storage volume.
+
+        @inherits :class:`NodeDriver.list_volume_snapshots`
+
+        :param region: The region in which to look for snapshots
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+        """
+        response = self.connection.request('/snapshots', region=region,
+                                           paged=True)
+        snapshots = filter(lambda s: s['base_volume']['id'] == volume.id,
+                           response.object['snapshots'])
+        return [self._to_snapshot(snapshot) for snapshot in snapshots]
+
+    def _to_snapshot(self, snapshot):
+        state = self.SNAPSHOT_STATE_MAP.get(snapshot['state'],
+                                            VolumeSnapshotState.UNKNOWN)
+        extra = {
+            'organization': snapshot['organization'],
+            'volume_type': snapshot['volume_type'],
+        }
+        return VolumeSnapshot(id=snapshot['id'],
+                              driver=self,
+                              size=_to_lib_size(snapshot['size']),
+                              created=parse_date(snapshot['creation_date']),
+                              state=state,
+                              extra=extra)
+
+    def create_volume(self, size, name, region=None):
+        """
+        Create a new volume.
+
+        :param size: Size of volume in gigabytes.
+        :type size: ``int``
+
+        :param name: Name of the volume to be created.
+        :type name: ``str``
+
+        :param region: The region in which to create the volume
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: The newly created volume.
+        :rtype: :class:`StorageVolume`
+        """
+        data = {
+            'name': name,
+            'organization': self.key,
+            'volume_type': 'l_ssd',
+            'size': _to_api_size(size)
+        }
+        response = self.connection.request('/volumes',
+                                           region=region,
+                                           data=json.dumps(data),
+                                           method='POST')
+        volume = response.object['volume']
+        return self._to_volume(volume)
+
+    def create_volume_snapshot(self, volume, name, region=None):
+        """
+        Create snapshot from volume.
+
+        :param volume: The volume to create a snapshot from
+        :type volume: :class`StorageVolume`
+
+        :param name: The name to give the snapshot
+        :type name: ``str``
+
+        :param region: The region in which to create the snapshot
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: The newly created snapshot.
+        :rtype: :class:`VolumeSnapshot`
+        """
+        data = {
+            'name': name,
+            'organization': self.key,
+            'volume_id': volume.id
+        }
+        response = self.connection.request('/snapshots',
+                                           region=region,
+                                           data=json.dumps(data),
+                                           method='POST')
+        snapshot = response.object['snapshot']
+        return self._to_snapshot(snapshot)
+
+    def destroy_volume(self, volume, region=None):
+        """
+        Destroys a storage volume.
+
+        :param volume: Volume to be destroyed
+        :type volume: :class:`StorageVolume`
+
+        :param region: The region in which to look for the volume
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: True if the destroy was successful, otherwise False
+        :rtype: ``bool``
+        """
+        return self.connection.request('/volumes/%s' % volume.id,
+                                       region=region,
+                                       method='DELETE').success()
+
+    def destroy_volume_snapshot(self, snapshot, region=None):
+        """
+        Dostroy a volume snapshot
+
+        :param snapshot: volume snapshot to destroy
+        :type snapshot: class:`VolumeSnapshot`
+
+        :param region: The region in which to look for the snapshot
+        (if None, use default region specified in __init__)
+        :type region: :class:`.NodeLocation`
+
+        :return: True if the destroy was successful, otherwise False
+        :rtype: ``bool``
+        """
+        return self.connection.request('/snapshots/%s' % snapshot.id,
+                                       region=region,
+                                       method='DELETE').success()
+
+    def list_key_pairs(self):
+        """
+        List all the available SSH keys.
+
+        :return: Available SSH keys.
+        :rtype: ``list`` of :class:`KeyPair`
+        """
+        response = self.connection.request('/users/%s' % (self._get_user_id()),
+                                           region='account')
+        keys = response.object['user']['ssh_public_keys']
+        return [KeyPair(name=' '.join(key['key'].split(' ')[2:]),
+                        public_key=' '.join(key['key'].split(' ')[:2]),
+                        fingerprint=key['fingerprint'],
+                        driver=self) for key in keys]
+
+    def import_key_pair_from_string(self, name, key_material):
+        """
+        Import a new public key from string.
+
+        :param name: Key pair name.
+        :type name: ``str``
+
+        :param key_material: Public key material.
+        :type key_material: ``str``
+
+        :return: Imported key pair object.
+        :rtype: :class:`KeyPair`
+        """
+        new_key = KeyPair(name=name,
+                          public_key=' '.join(key_material.split(' ')[:2]),
+                          fingerprint=None,
+                          driver=self)
+        keys = [key for key in self.list_key_pairs() if not key.name == name]
+        keys.append(new_key)
+        return self._save_keys(keys)
+
+    def delete_key_pair(self, key_pair):
+        """
+        Delete an existing key pair.
+
+        :param key_pair: Key pair object.
+        :type key_pair: :class:`KeyPair`
+
+        :return:   True of False based on success of Keypair deletion
+        :rtype:    ``bool``
+        """
+        keys = [key for key in self.list_key_pairs()
+                if not key.name == key_pair.name]
+        return self._save_keys(keys)
+
+    def _get_user_id(self):
+        response = self.connection.request('/tokens/%s' % self.secret,
+                                           region='account')
+        return response.object['token']['user_id']
+
+    def _save_keys(self, keys):
+        data = {
+            'ssh_public_keys': [{'key': '%s %s' % (key.public_key, key.name)}
+                                for key in keys]
+        }
+        response = self.connection.request('/users/%s' % (self._get_user_id()),
+                                           region='account',
+                                           method='PATCH',
+                                           data=json.dumps(data))
+        return response.success()

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/compute/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/providers.py b/libcloud/compute/providers.py
index 72a5a85..c154d2d 100644
--- a/libcloud/compute/providers.py
+++ b/libcloud/compute/providers.py
@@ -147,6 +147,8 @@ DRIVERS = {
     ('libcloud.compute.drivers.oneandone', 'OneAndOneNodeDriver'),
     Provider.UPCLOUD:
     ('libcloud.compute.drivers.upcloud', 'UpcloudDriver'),
+    Provider.SCALEWAY:
+    ('libcloud.compute.drivers.scaleway', 'ScalewayNodeDriver'),
 }
 
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/compute/types.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py
index 44e8687..0c6bb1c 100644
--- a/libcloud/compute/types.py
+++ b/libcloud/compute/types.py
@@ -158,6 +158,7 @@ class Provider(Type):
     RACKSPACE_FIRST_GEN = 'rackspace_first_gen'
     RIMUHOSTING = 'rimuhosting'
     RUNABOVE = 'runabove'
+    SCALEWAY = 'scaleway'
     SERVERLOVE = 'serverlove'
     SKALICLOUD = 'skalicloud'
     SOFTLAYER = 'softlayer'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/create_image.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/create_image.json b/libcloud/test/compute/fixtures/scaleway/create_image.json
new file mode 100644
index 0000000..3ed8174
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/create_image.json
@@ -0,0 +1,21 @@
+{
+  "image": {
+    "arch": "arm",
+    "creation_date": "2014-05-22T12:56:56.984011+00:00",
+    "extra_volumes": "[]",
+    "from_image": null,
+    "from_server": null,
+    "id": "98bf3ac2-a1f5-471d-8c8f-1b706ab57ef0",
+    "marketplace_key": null,
+    "modification_date": "2014-05-22T12:56:56.984011+00:00",
+    "name": "my_image",
+    "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+    "public": false,
+    "root_volume": {
+      "size": 25000000000,
+      "id": "f0361e7b-cbe4-4882-a999-945192b7171b",
+      "volume_type": "l_ssd",
+      "name": "vol-0-1"
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/create_node.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/create_node.json b/libcloud/test/compute/fixtures/scaleway/create_node.json
new file mode 100644
index 0000000..b6897ee
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/create_node.json
@@ -0,0 +1,40 @@
+{
+  "server": {
+    "bootscript": null,
+    "creation_date": "2014-05-22T12:57:22.514299+00:00",
+    "dynamic_ip_required": true,
+    "id": "741db378",
+    "image": {
+      "id": "85917034-46b0-4cc5-8b48-f0a2245e357e",
+      "name": "ubuntu working"
+    },
+    "location": null,
+    "name": "my_server",
+    "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+    "private_ip": null,
+    "public_ip": null,
+    "enable_ipv6": true,
+    "state": "stopped",
+    "ipv6": null,
+    "commercial_type": "VC1S",
+    "arch": "x86_64",
+    "tags": [
+      "test",
+      "www"
+    ],
+    "volumes": {
+      "0": {
+        "export_uri": null,
+        "id": "d9257116-6919-49b4-a420-dcfdff51fcb1",
+        "name": "vol simple snapshot",
+        "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+        "server": {
+          "id": "3cb18e2d-f4f7-48f7-b452-59b88ae8fc8c",
+          "name": "my_server"
+        },
+        "size": 10000000000,
+        "volume_type": "l_ssd"
+      }
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/create_volume.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/create_volume.json b/libcloud/test/compute/fixtures/scaleway/create_volume.json
new file mode 100644
index 0000000..ac4dbcf
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/create_volume.json
@@ -0,0 +1,13 @@
+{
+  "volume": {
+    "creation_date": "2014-05-22T12:57:22.514299+00:00",
+    "modification_date": "2014-05-22T12:57:22.514299+00:00",
+    "export_uri": null,
+    "id": "c675f420-cfeb-48ff-ba2a-9d2a4dbe3fcd",
+    "name": "volume-0-3",
+    "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+    "server": null,
+    "size": 10000000000,
+    "volume_type": "l_ssd"
+  }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/create_volume_snapshot.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/create_volume_snapshot.json b/libcloud/test/compute/fixtures/scaleway/create_volume_snapshot.json
new file mode 100644
index 0000000..9d92d52
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/create_volume_snapshot.json
@@ -0,0 +1,15 @@
+{
+  "snapshot": {
+    "base_volume": {
+      "id": "f929fe39-63f8-4be8-a80e-1e9c8ae22a76",
+      "name": "vol simple snapshot"
+    },
+    "creation_date": "2014-05-22T12:10:05.596769+00:00",
+    "id": "f0361e7b-cbe4-4882-a999-945192b7171b",
+    "name": "snapshot-0-1",
+    "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+    "size": 10000000000,
+    "state": "snapshotting",
+    "volume_type": "l_ssd"
+  }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/error.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/error.json b/libcloud/test/compute/fixtures/scaleway/error.json
new file mode 100644
index 0000000..2b21f5d
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/error.json
@@ -0,0 +1 @@
+{"message": "Authentication error", "type": "invalid_auth"}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/error_invalid_image.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/error_invalid_image.json b/libcloud/test/compute/fixtures/scaleway/error_invalid_image.json
new file mode 100644
index 0000000..2a07559
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/error_invalid_image.json
@@ -0,0 +1 @@
+{"message": "\"01234567-89ab-cdef-fedc-ba9876543210\" not found", "type": "unknown_resource"}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/get_image.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/get_image.json b/libcloud/test/compute/fixtures/scaleway/get_image.json
new file mode 100644
index 0000000..df98b3d
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/get_image.json
@@ -0,0 +1,21 @@
+{
+  "image": {
+    "arch": "arm",
+    "creation_date": "2014-05-22T12:56:56.984011+00:00",
+    "extra_volumes": "[]",
+    "from_image": null,
+    "from_server": null,
+    "id": "12345",
+    "marketplace_key": null,
+    "modification_date": "2014-05-22T12:56:56.984011+00:00",
+    "name": "my_image",
+    "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+    "public": false,
+    "root_volume": {
+      "size": 25000000000,
+      "id": "f0361e7b-cbe4-4882-a999-945192b7171b",
+      "volume_type": "l_ssd",
+      "name": "vol-0-1"
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/list_availability.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/list_availability.json b/libcloud/test/compute/fixtures/scaleway/list_availability.json
new file mode 100644
index 0000000..cdef1a5
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/list_availability.json
@@ -0,0 +1,13 @@
+{
+  "servers": {
+    "X64-120GB": {
+      "availability": "scarce"
+    },
+    "START1-XS": {
+      "availability": "available"
+    },
+    "ARM64-4GB": {
+      "availability": "shortage"
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/list_images.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/list_images.json b/libcloud/test/compute/fixtures/scaleway/list_images.json
new file mode 100644
index 0000000..1151d82
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/list_images.json
@@ -0,0 +1,42 @@
+{
+  "images": [
+    {
+      "arch": "arm",
+      "creation_date": "2014-05-22T12:56:56.984011+00:00",
+      "extra_volumes": "[]",
+      "from_image": null,
+      "from_server": null,
+      "id": "12345",
+      "marketplace_key": null,
+      "modification_date": "2014-05-22T12:56:56.984011+00:00",
+      "name": "my_image",
+      "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+      "public": false,
+      "root_volume": {
+        "size": 50000000000,
+        "id": "f0361e7b-cbe4-4882-a999-945192b7171b",
+        "volume_type": "l_ssd",
+        "name": "vol-0-1"
+      }
+    },
+    {
+      "arch": "arm",
+      "creation_date": "2014-05-22T12:57:22.514299+00:00",
+      "extra_volumes": "[]",
+      "from_image": null,
+      "from_server": null,
+      "id": "54321",
+      "marketplace_key": null,
+      "modification_date": "2014-05-22T12:57:22.514299+00:00",
+      "name": "my_image_1",
+      "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+      "public": false,
+      "root_volume": {
+        "size": 25000000000,
+        "id": "f0361e7b-cbe4-4882-a999-945192b7171b",
+        "volume_type": "l_ssd",
+        "name": "vol-0-2"
+      }
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/list_nodes.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/list_nodes.json b/libcloud/test/compute/fixtures/scaleway/list_nodes.json
new file mode 100644
index 0000000..bc4019d
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/list_nodes.json
@@ -0,0 +1,74 @@
+{
+  "servers": [
+    {
+      "bootscript": null,
+      "arch": "arm",
+      "creation_date": "2014-05-22T12:57:22.514299+00:00",
+      "dynamic_public_ip": false,
+      "id": "741db378",
+      "image": {
+        "id": "85917034-46b0-4cc5-8b48-f0a2245e357e",
+        "name": "ubuntu working"
+      },
+      "location": null,
+      "name": "my_server",
+      "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+      "private_ip": null,
+      "public_ip": null,
+      "state": "running",
+      "tags": [
+        "test",
+        "www"
+      ],
+      "volumes": {
+        "0": {
+          "export_uri": null,
+          "id": "c1eb8f3a-4f0b-4b95-a71c-93223e457f5a",
+          "name": "vol simple snapshot",
+          "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+          "server": {
+            "id": "741db378-6b87-46d4-a8c5-4e46a09ab1f8",
+            "name": "my_server"
+          },
+          "size": 10000000000,
+          "volume_type": "l_ssd"
+        }
+      }
+    },
+    {
+      "bootscript": null,
+      "arch": "arm",
+      "creation_date": "2014-05-22T12:57:22.514299+00:00",
+      "dynamic_public_ip": false,
+      "id": "0e9f85af",
+      "image": {
+        "id": "85917034-46b0-4cc5-8b48-f0a2245e357e",
+        "name": "ubuntu working"
+      },
+      "location": null,
+      "name": "my_server",
+      "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+      "private_ip": null,
+      "public_ip": null,
+      "state": "running",
+      "tags": [
+        "test",
+        "www"
+      ],
+      "volumes": {
+        "0": {
+          "export_uri": null,
+          "id": "fb09bb31-ecd9-4dff-8b55-b6e45715199d",
+          "name": "vol simple snapshot",
+          "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+          "server": {
+            "id": "0e9f85af-b6aa-401e-a00d-484f832c5024",
+            "name": "my_server"
+          },
+          "size": 10000000000,
+          "volume_type": "l_ssd"
+        }
+      }
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/list_nodes_empty.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/list_nodes_empty.json b/libcloud/test/compute/fixtures/scaleway/list_nodes_empty.json
new file mode 100644
index 0000000..b69ad4f
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/list_nodes_empty.json
@@ -0,0 +1,3 @@
+{
+  "servers": []
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/list_sizes.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/list_sizes.json b/libcloud/test/compute/fixtures/scaleway/list_sizes.json
new file mode 100644
index 0000000..650d0f8
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/list_sizes.json
@@ -0,0 +1,76 @@
+{
+  "servers": {
+    "X64-120GB": {
+      "baremetal": false,
+      "monthly_price": null,
+      "volumes_constraint": {
+        "min_size": 500000000000,
+        "max_size": 1000000000000
+      },
+      "network": {
+        "interfaces": [
+          {
+            "internal_bandwidth": null,
+            "internet_bandwidth": 1073741824
+          }
+        ],
+        "sum_internal_bandwidth": null,
+        "sum_internet_bandwidth": 1073741824,
+        "ipv6_support": true
+      },
+      "hourly_price": null,
+      "ncpus": 12,
+      "ram": 128849018880,
+      "arch": "x86_64",
+      "alt_names": []
+    },
+    "START1-XS": {
+      "baremetal": false,
+      "monthly_price": 1.99,
+      "volumes_constraint": {
+        "min_size": 25000000000,
+        "max_size": 25000000000
+      },
+      "network": {
+        "interfaces": [
+          {
+            "internal_bandwidth": null,
+            "internet_bandwidth": 104857600
+          }
+        ],
+        "sum_internal_bandwidth": null,
+        "sum_internet_bandwidth": 104857600,
+        "ipv6_support": true
+      },
+      "hourly_price": 0.004,
+      "ncpus": 1,
+      "ram": 1073741824,
+      "arch": "x86_64",
+      "alt_names": []
+    },
+    "ARM64-4GB": {
+      "baremetal": false,
+      "monthly_price": 5.99,
+      "volumes_constraint": {
+        "min_size": 100000000000,
+        "max_size": 100000000000
+      },
+      "network": {
+        "interfaces": [
+          {
+            "internal_bandwidth": null,
+            "internet_bandwidth": 209715200
+          }
+        ],
+        "sum_internal_bandwidth": null,
+        "sum_internet_bandwidth": 209715200,
+        "ipv6_support": true
+      },
+      "hourly_price": 0.012,
+      "ncpus": 6,
+      "ram": 4294967296,
+      "arch": "arm64",
+      "alt_names": []
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/list_volume_snapshots.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/list_volume_snapshots.json b/libcloud/test/compute/fixtures/scaleway/list_volume_snapshots.json
new file mode 100644
index 0000000..3db699d
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/list_volume_snapshots.json
@@ -0,0 +1,30 @@
+{
+  "snapshots": [
+    {
+      "base_volume": {
+        "id": "f929fe39-63f8-4be8-a80e-1e9c8ae22a76",
+        "name": "vol simple snapshot"
+      },
+      "creation_date": "2014-05-22T12:11:06.055998+00:00",
+      "id": "6f418e5f-b42d-4423-a0b5-349c74c454a4",
+      "name": "snapshot-0-1",
+      "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+      "size": 10000000000,
+      "state": "snapshotting",
+      "volume_type": "l_ssd"
+    },
+    {
+      "base_volume": {
+        "id": "f929fe39-63f8-4be8-a80e-1e9c8ae22a76",
+        "name": "vol simple snapshot"
+      },
+      "creation_date": "2014-05-22T12:13:09.877961+00:00",
+      "id": "c6ff5501-eb35-44b8-aa01-8777211a830b",
+      "name": "snapshot-0-2",
+      "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+      "size": 10000000000,
+      "state": "snapshotting",
+      "volume_type": "l_ssd"
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/list_volumes.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/list_volumes.json b/libcloud/test/compute/fixtures/scaleway/list_volumes.json
new file mode 100644
index 0000000..0af7b38
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/list_volumes.json
@@ -0,0 +1,26 @@
+{
+  "volumes": [
+    {
+      "export_uri": null,
+      "creation_date": "2014-05-22T12:56:56.984011+00:00",
+      "modification_date": "2014-05-22T12:56:56.984011+00:00",
+      "id": "f929fe39-63f8-4be8-a80e-1e9c8ae22a76",
+      "name": "volume-0-1",
+      "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+      "server": null,
+      "size": 10000000000,
+      "volume_type": "l_ssd"
+    },
+    {
+      "export_uri": null,
+      "creation_date": "2014-05-22T12:56:56.984011+00:00",
+      "modification_date": "2014-05-22T12:56:56.984011+00:00",
+      "id": "0facb6b5-b117-441a-81c1-f28b1d723779",
+      "name": "volume-0-2",
+      "organization": "000a115d-2852-4b0a-9ce8-47f1134ba95a",
+      "server": null,
+      "size": 20000000000,
+      "volume_type": "l_ssd"
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/list_volumes_empty.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/list_volumes_empty.json b/libcloud/test/compute/fixtures/scaleway/list_volumes_empty.json
new file mode 100644
index 0000000..67995cb
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/list_volumes_empty.json
@@ -0,0 +1,3 @@
+{
+  "volumes": []
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/reboot_node.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/reboot_node.json b/libcloud/test/compute/fixtures/scaleway/reboot_node.json
new file mode 100644
index 0000000..fe0b421
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/reboot_node.json
@@ -0,0 +1,9 @@
+{
+  "task": {
+    "description": "server_reboot",
+    "href_from": "/servers/741db378/action",
+    "id": "741db378",
+    "progress": "0",
+    "status": "pending"
+  }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/token_info.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/token_info.json b/libcloud/test/compute/fixtures/scaleway/token_info.json
new file mode 100644
index 0000000..95b5b52
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/token_info.json
@@ -0,0 +1,14 @@
+{
+  "token": {
+    "creation_date": "2014-05-22T08:06:51.742826+00:00",
+    "expires": "2014-05-20T14:05:06.393875+00:00",
+    "id": "token",
+    "inherits_user_perms": true,
+    "permissions": [],
+    "roles": {
+      "organization": null,
+      "role": null
+    },
+    "user_id": "5bea0358"
+  }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/fixtures/scaleway/user_info.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/scaleway/user_info.json b/libcloud/test/compute/fixtures/scaleway/user_info.json
new file mode 100644
index 0000000..ec02901
--- /dev/null
+++ b/libcloud/test/compute/fixtures/scaleway/user_info.json
@@ -0,0 +1,15 @@
+{
+	"user": {
+		"email": "jsnow@got.com",
+		"firstname": "John",
+		"fullname": "John Snow",
+		"id": "5bea0358",
+		"lastname": "Snow",
+		"organizations": null,
+		"roles": null,
+		"ssh_public_keys": [{
+			"fingerprint": "f5:d1:78:ed:28:72:5f:e1:ac:94:fd:1f:e0:a3:48:6d",
+			"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDGk5 example"
+		}]
+	}
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/compute/test_scaleway.py
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/test_scaleway.py b/libcloud/test/compute/test_scaleway.py
new file mode 100644
index 0000000..3c2d0ea
--- /dev/null
+++ b/libcloud/test/compute/test_scaleway.py
@@ -0,0 +1,334 @@
+# 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
+
+from datetime import datetime
+from libcloud.utils.iso8601 import UTC
+
+try:
+    import simplejson as json
+except ImportError:
+    import json  # NOQA
+
+from libcloud.utils.py3 import httplib
+
+from libcloud.common.exceptions import BaseHTTPError
+from libcloud.compute.base import NodeImage
+from libcloud.compute.drivers.scaleway import ScalewayNodeDriver
+
+from libcloud.test import LibcloudTestCase, MockHttp
+from libcloud.test.file_fixtures import ComputeFileFixtures
+from libcloud.test.secrets import SCALEWAY_PARAMS
+
+
+# class ScalewayTests(unittest.TestCase, TestCaseMixin):
+class Scaleway_Tests(LibcloudTestCase):
+
+    def setUp(self):
+        ScalewayNodeDriver.connectionCls.conn_class = ScalewayMockHttp
+        ScalewayMockHttp.type = None
+        self.driver = ScalewayNodeDriver(*SCALEWAY_PARAMS)
+
+    def test_authentication(self):
+        ScalewayMockHttp.type = 'UNAUTHORIZED'
+        self.assertRaisesRegexp(BaseHTTPError, 'Authentication error',
+                                self.driver.list_nodes)
+
+    def test_list_locations_success(self):
+        locations = self.driver.list_locations()
+        self.assertTrue(len(locations) >= 1)
+
+        location = locations[0]
+        self.assertEqual(location.id, 'par1')
+        self.assertEqual(location.name, 'Paris 1')
+
+    def test_list_sizes_success(self):
+        sizes = self.driver.list_sizes()
+        self.assertTrue(len(sizes) >= 1)
+
+        size = sizes[0]
+        self.assertTrue(size.id is not None)
+        self.assertEqual(size.name, 'ARM64-4GB')
+        self.assertEqual(size.ram, 4096)
+
+        size = sizes[1]
+        self.assertTrue(size.id is not None)
+        self.assertEqual(size.name, 'START1-XS')
+        self.assertEqual(size.ram, 1024)
+
+        size = sizes[2]
+        self.assertTrue(size.id is not None)
+        self.assertEqual(size.name, 'X64-120GB')
+        self.assertEqual(size.ram, 122880)
+
+    def test_list_images_success(self):
+        images = self.driver.list_images()
+        self.assertTrue(len(images) >= 1)
+
+        image = images[0]
+        self.assertTrue(image.id is not None)
+        self.assertTrue(image.name is not None)
+
+    def test_create_image_success(self):
+        node = self.driver.list_nodes()[0]
+        ScalewayMockHttp.type = 'POST'
+        image = self.driver.create_image(node, 'my_image')
+        self.assertEqual(image.name, 'my_image')
+        self.assertEqual(image.id, '98bf3ac2-a1f5-471d-8c8f-1b706ab57ef0')
+        self.assertEqual(image.extra['arch'], 'arm')
+
+    def test_delete_image_success(self):
+        image = self.driver.get_image(12345)
+        ScalewayMockHttp.type = 'DELETE'
+        result = self.driver.delete_image(image)
+        self.assertTrue(result)
+
+    def test_get_image_success(self):
+        image = self.driver.get_image(12345)
+        self.assertEqual(image.name, 'my_image')
+        self.assertEqual(image.id, '12345')
+        self.assertEqual(image.extra['arch'], 'arm')
+
+    def test_list_nodes_success(self):
+        nodes = self.driver.list_nodes()
+        self.assertEqual(len(nodes), 2)
+        self.assertEqual(nodes[0].name, 'my_server')
+        self.assertEqual(nodes[0].public_ips, [])
+        self.assertEqual(nodes[0].extra['volumes']['0']['id'], "c1eb8f3a-4f0b-4b95-a71c-93223e457f5a")
+        self.assertEqual(nodes[0].extra['organization'], '000a115d-2852-4b0a-9ce8-47f1134ba95a')
+
+    def test_list_nodes_fills_created_datetime(self):
+        nodes = self.driver.list_nodes()
+        self.assertEqual(nodes[0].created_at, datetime(2014, 5, 22, 12, 57, 22,
+                                                       514298, tzinfo=UTC))
+
+    def test_create_node_success(self):
+        image = self.driver.list_images()[0]
+        size = self.driver.list_sizes()[0]
+        location = self.driver.list_locations()[0]
+
+        ScalewayMockHttp.type = 'POST'
+        node = self.driver.create_node(name='test', size=size, image=image,
+                                       region=location)
+        self.assertEqual(node.name, 'my_server')
+        self.assertEqual(node.public_ips, [])
+        self.assertEqual(node.extra['volumes']['0']['id'], "d9257116-6919-49b4-a420-dcfdff51fcb1")
+        self.assertEqual(node.extra['organization'], '000a115d-2852-4b0a-9ce8-47f1134ba95a')
+
+    def test_create_node_invalid_size(self):
+        image = NodeImage(id='01234567-89ab-cdef-fedc-ba9876543210', name=None,
+                          driver=self.driver)
+        size = self.driver.list_sizes()[0]
+        location = self.driver.list_locations()[0]
+
+        ScalewayMockHttp.type = 'INVALID_IMAGE'
+        expected_msg = '" not found'
+        self.assertRaisesRegexp(Exception, expected_msg,
+                                self.driver.create_node,
+                                name='test', size=size, image=image,
+                                region=location)
+
+    def test_reboot_node_success(self):
+        node = self.driver.list_nodes()[0]
+        ScalewayMockHttp.type = 'REBOOT'
+        result = self.driver.reboot_node(node)
+        self.assertTrue(result)
+
+    def test_destroy_node_success(self):
+        node = self.driver.list_nodes()[0]
+        ScalewayMockHttp.type = 'TERMINATE'
+        result = self.driver.destroy_node(node)
+        self.assertTrue(result)
+
+    def test_list_volumes(self):
+        volumes = self.driver.list_volumes()
+        self.assertEqual(len(volumes), 2)
+        volume = volumes[0]
+        self.assertEqual(volume.id, "f929fe39-63f8-4be8-a80e-1e9c8ae22a76")
+        self.assertEqual(volume.name, "volume-0-1")
+        self.assertEqual(volume.size, 10)
+        self.assertEqual(volume.driver, self.driver)
+
+    def test_list_volumes_empty(self):
+        ScalewayMockHttp.type = 'EMPTY'
+        volumes = self.driver.list_volumes()
+        self.assertEqual(len(volumes), 0)
+
+    def test_list_volume_snapshots(self):
+        volume = self.driver.list_volumes()[0]
+        snapshots = self.driver.list_volume_snapshots(volume)
+        self.assertEqual(len(snapshots), 2)
+        snapshot1, snapshot2 = snapshots
+        self.assertEqual(snapshot1.id, "6f418e5f-b42d-4423-a0b5-349c74c454a4")
+        self.assertEqual(snapshot2.id, "c6ff5501-eb35-44b8-aa01-8777211a830b")
+
+    def test_create_volume(self):
+        par1 = [r for r in self.driver.list_locations() if r.id == 'par1'][0]
+        ScalewayMockHttp.type = 'POST'
+        volume = self.driver.create_volume(10, 'volume-0-3', par1)
+        self.assertEqual(volume.id, "c675f420-cfeb-48ff-ba2a-9d2a4dbe3fcd")
+        self.assertEqual(volume.name, "volume-0-3")
+        self.assertEqual(volume.size, 10)
+        self.assertEqual(volume.driver, self.driver)
+
+    def test_create_volume_snapshot(self):
+        volume = self.driver.list_volumes()[0]
+        ScalewayMockHttp.type = 'POST'
+        snapshot = self.driver.create_volume_snapshot(volume, 'snapshot-0-1')
+        self.assertEqual(snapshot.id, "f0361e7b-cbe4-4882-a999-945192b7171b")
+        self.assertEqual(snapshot.extra['volume_type'], 'l_ssd')
+        self.assertEqual(volume.driver, self.driver)
+
+    def test_destroy_volume(self):
+        volume = self.driver.list_volumes()[0]
+        ScalewayMockHttp.type = 'DELETE'
+        resp = self.driver.destroy_volume(volume)
+        self.assertTrue(resp)
+
+    def test_destroy_volume_snapshot(self):
+        volume = self.driver.list_volumes()[0]
+        snapshot = self.driver.list_volume_snapshots(volume)[0]
+        ScalewayMockHttp.type = 'DELETE'
+        result = self.driver.destroy_volume_snapshot(snapshot)
+        self.assertTrue(result)
+
+    def test_list_key_pairs(self):
+        keys = self.driver.list_key_pairs()
+        self.assertEqual(len(keys), 1)
+        self.assertEqual(keys[0].name, 'example')
+        self.assertEqual(keys[0].public_key,
+                         "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDGk5")
+        self.assertEqual(keys[0].fingerprint,
+                         "f5:d1:78:ed:28:72:5f:e1:ac:94:fd:1f:e0:a3:48:6d")
+
+    def test_import_key_pair_from_string(self):
+        result = self.driver.import_key_pair_from_string(
+            name="example",
+            key_material="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDGk5"
+        )
+        self.assertTrue(result)
+
+    def test_delete_key_pair(self):
+        key = self.driver.list_key_pairs()[0]
+        result = self.driver.delete_key_pair(key)
+        self.assertTrue(result)
+
+
+class ScalewayMockHttp(MockHttp):
+    fixtures = ComputeFileFixtures('scaleway')
+
+    def _products_servers(self, method, url, body, headers):
+        body = self.fixtures.load('list_sizes.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _products_servers_availability(self, method, url, body, headers):
+        body = self.fixtures.load('list_availability.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _servers_UNAUTHORIZED(self, method, url, body, headers):
+        body = self.fixtures.load('error.json')
+        return (httplib.UNAUTHORIZED, body, {},
+                httplib.responses[httplib.UNAUTHORIZED])
+
+    def _images(self, method, url, body, headers):
+        body = self.fixtures.load('list_images.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _images_POST(self, method, url, body, headers):
+        # create_image
+        body = self.fixtures.load('create_image.json')
+        return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED])
+
+    def _images_12345_DELETE(self, method, url, body, headers):
+        # delete_image
+        return (httplib.NO_CONTENT, body, {},
+                httplib.responses[httplib.NO_CONTENT])
+
+    def _images_12345(self, method, url, body, headers):
+        # get_image
+        body = self.fixtures.load('get_image.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _servers(self, method, url, body, headers):
+        body = self.fixtures.load('list_nodes.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _servers_POST(self, method, url, body, headers):
+        body = self.fixtures.load('create_node.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _servers_741db378_action_POST(self, method, url, body, headers):
+        # reboot_node
+        return (httplib.NO_CONTENT, body, {},
+                httplib.responses[httplib.NO_CONTENT])
+
+    def _servers_INVALID_IMAGE(self, method, url, body, headers):
+        body = self.fixtures.load('error_invalid_image.json')
+        return (httplib.NOT_FOUND, body, {},
+                httplib.responses[httplib.NOT_FOUND])
+
+    def _servers_741db378_action_REBOOT(self, method, url, body, headers):
+        # reboot_node
+        body = self.fixtures.load('reboot_node.json')
+        return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED])
+
+    def _servers_741db378_action_TERMINATE(self, method, url, body, headers):
+        # destroy_node
+        return (httplib.NO_CONTENT, body, {},
+                httplib.responses[httplib.NO_CONTENT])
+
+    def _volumes(self, method, url, body, headers):
+        body = self.fixtures.load('list_volumes.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _volumes_EMPTY(self, method, url, body, headers):
+        body = self.fixtures.load('list_volumes_empty.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _snapshots(
+            self, method, url, body, headers):
+        body = self.fixtures.load('list_volume_snapshots.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _volumes_POST(self, method, url, body, headers):
+        body = self.fixtures.load('create_volume.json')
+        return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED])
+
+    def _snapshots_POST(self, method, url, body, headers):
+        body = self.fixtures.load('create_volume_snapshot.json')
+        return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED])
+
+    def _volumes_f929fe39_63f8_4be8_a80e_1e9c8ae22a76_DELETE(
+            self, method, url, body, headers):
+        return (httplib.NO_CONTENT, None, {},
+                httplib.responses[httplib.NO_CONTENT])
+
+    def _snapshots_6f418e5f_b42d_4423_a0b5_349c74c454a4_DELETE(
+            self, method, url, body, headers):
+        return (httplib.NO_CONTENT, None, {},
+                httplib.responses[httplib.NO_CONTENT])
+
+    def _tokens_token(self, method, url, body, headers):
+        body = self.fixtures.load('token_info.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _users_5bea0358(self, method, url, body, headers):
+        body = self.fixtures.load('user_info.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/3315e976/libcloud/test/secrets.py-dist
----------------------------------------------------------------------
diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist
index 8eaa0c6..7076a6c 100644
--- a/libcloud/test/secrets.py-dist
+++ b/libcloud/test/secrets.py-dist
@@ -25,14 +25,14 @@ GCE_PARAMS = ('email@developer.gserviceaccount.com', 'key')  # Service Account A
 # GCE_PARAMS = ('client_id', 'client_secret')  # Installed App Authentication
 GCE_KEYWORD_PARAMS = {'project': 'project_name'}
 GKE_PARAMS = ('email@developer.gserviceaccount.com', 'key')  # Service Account Authentication
-# GCE_PARAMS = ('client_id', 'client_secret')  # Installed App Authentication
+# GKE_PARAMS = ('client_id', 'client_secret')  # Installed App Authentication
 GKE_KEYWORD_PARAMS = {'project': 'project_name'}
 
 HOSTINGCOM_PARAMS = ('user', 'secret')
 IBM_PARAMS = ('user', 'secret')
 ONAPP_PARAMS = ('key')
-# OPENSTACK_PARAMS = ('user_name', 'api_key', secure_bool, 'host', port_int)
 ONEANDONE_PARAMS = ('token')
+# OPENSTACK_PARAMS = ('user_name', 'api_key', secure_bool, 'host', port_int)
 OPENSTACK_PARAMS = ('user_name', 'api_key', False, 'host', 8774)
 OPENNEBULA_PARAMS = ('user', 'key')
 DIMENSIONDATA_PARAMS = ('user', 'password')
@@ -41,6 +41,7 @@ OVH_PARAMS = ('application_key', 'application_secret', 'project_id', 'consumer_k
 RACKSPACE_PARAMS = ('user', 'key')
 RACKSPACE_NOVA_PARAMS = ('user_name', 'api_key', False, 'host', 8774)
 SLICEHOST_PARAMS = ('key',)
+SCALEWAY_PARAMS = ('access_key', 'token')
 SOFTLAYER_PARAMS = ('user', 'api_key')
 VCLOUD_PARAMS = ('user', 'secret')
 VOXEL_PARAMS = ('key', 'secret')


Mime
View raw message