From notifications-return-14588-archive-asf-public=cust-asf.ponee.io@libcloud.apache.org Mon Jun 11 21:03:22 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id 0A226180647 for ; Mon, 11 Jun 2018 21:03:19 +0200 (CEST) Received: (qmail 40972 invoked by uid 500); 11 Jun 2018 19:03:18 -0000 Mailing-List: contact notifications-help@libcloud.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@libcloud.apache.org Delivered-To: mailing list notifications@libcloud.apache.org Received: (qmail 40959 invoked by uid 500); 11 Jun 2018 19:03:18 -0000 Delivered-To: apmail-libcloud-commits@libcloud.apache.org Received: (qmail 40956 invoked by uid 99); 11 Jun 2018 19:03:18 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 11 Jun 2018 19:03:18 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id B5659E0C56; Mon, 11 Jun 2018 19:03:18 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: quentinp@apache.org To: commits@libcloud.apache.org Date: Mon, 11 Jun 2018 19:03:18 -0000 Message-Id: X-Mailer: ASF-Git Admin Mailer Subject: [1/4] libcloud git commit: Scaleway Compute Driver (squashed) 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 Authored: Mon May 21 13:11:26 2018 -0600 Committer: Daniel Hunsaker 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')