You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by qu...@apache.org on 2017/09/27 03:12:47 UTC

[04/14] libcloud git commit: New compute driver for UpCloud

New compute driver for UpCloud

Signed-off-by: Quentin Pradet <qu...@apache.org>


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

Branch: refs/heads/trunk
Commit: abbd9bb63f8b1295bc67fbcf8f271f89ed79e4d6
Parents: 5892fa1
Author: Mika Lackman <mi...@upcloud.com>
Authored: Mon Aug 14 14:03:29 2017 +0300
Committer: Quentin Pradet <qu...@apache.org>
Committed: Wed Sep 27 07:04:33 2017 +0400

----------------------------------------------------------------------
 libcloud/common/upcloud.py                      | 183 +++++++++
 libcloud/compute/drivers/upcloud.py             | 277 +++++++++++++
 libcloud/compute/types.py                       |   2 +
 libcloud/test/common/test_upcloud.py            | 212 ++++++++++
 .../compute/fixtures/upcloud/api_1_2_plan.json  |  38 ++
 .../fixtures/upcloud/api_1_2_server.json        |  22 +
 ...er_00893c98-5d5a-4363-b177-88df518a2b60.json |  58 +++
 ...er_00f8c525-7e62-4108-8115-3958df5b43dc.json |  57 +++
 ...525-7e62-4108-8115-3958df5b43dc_restart.json |  57 +++
 .../upcloud/api_1_2_server_from_cdrom.json      |  65 +++
 .../upcloud/api_1_2_server_from_template.json   |  59 +++
 .../fixtures/upcloud/api_1_2_storage_cdrom.json | 411 +++++++++++++++++++
 .../upcloud/api_1_2_storage_template.json       | 114 +++++
 .../compute/fixtures/upcloud/api_1_2_zone.json  |  30 ++
 .../upcloud/api_1_2_zone_failed_auth.json       |   6 +
 libcloud/test/compute/test_upcloud.py           | 248 +++++++++++
 libcloud/test/secrets.py-dist                   |   1 +
 17 files changed, 1840 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/common/upcloud.py
----------------------------------------------------------------------
diff --git a/libcloud/common/upcloud.py b/libcloud/common/upcloud.py
new file mode 100644
index 0000000..432606a
--- /dev/null
+++ b/libcloud/common/upcloud.py
@@ -0,0 +1,183 @@
+# 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 json
+import time
+
+from libcloud.common.exceptions import BaseHTTPError
+
+
+class UpcloudTimeoutException(Exception):
+    pass
+
+
+class UpcloudCreateNodeRequestBody(object):
+    """Body of the create_node request
+
+    Takes the create_node arguments (**kwargs) and constructs the request body
+    """
+    def __init__(self, user_id, name, size, image, location, auth=None):
+        self.body = {
+            'server': {
+                'title': name,
+                'hostname': 'localhost',
+                'plan': size.id,
+                'zone': location.id,
+                'login_user': _LoginUser(user_id, auth).to_dict(),
+                'storage_devices': _StorageDevice(image, size).to_dict()
+            }
+        }
+
+    def to_json(self):
+        """Serializes the body to json"""
+        return json.dumps(self.body)
+
+
+class UpcloudNodeDestroyer(object):
+    """Destroyes the node.
+    Node must be first stopped and then it can be
+    destroyed"""
+
+    WAIT_AMOUNT = 2
+    SLEEP_COUNT_TO_TIMEOUT = 20
+
+    def __init__(self, upcloud_node_operations, sleep_func=None):
+        self._operations = upcloud_node_operations
+        self._sleep_func = sleep_func or time.sleep
+        self._sleep_count = 0
+
+    def destroy_node(self, node_id):
+        self._stop_called = False
+        self._sleep_count = 0
+        return self._do_destroy_node(node_id)
+
+    def _do_destroy_node(self, node_id):
+        state = self._operations.node_state(node_id)
+        if state == 'stopped':
+            self._operations.destroy_node(node_id)
+            return True
+        elif state == 'error':
+            return False
+        elif state == 'started':
+            if not self._stop_called:
+                self._operations.stop_node(node_id)
+                self._stop_called = True
+            else:
+                # Waiting for started state to change and
+                # not calling stop again
+                self._sleep()
+            return self._do_destroy_node(node_id)
+        elif state == 'maintenance':
+            # Lets wait maintenace state to go away and retry destroy
+            self._sleep()
+            return self._do_destroy_node(node_id)
+        elif state is None:  # Server not found any more
+            return True
+
+    def _sleep(self):
+        if self._sleep_count > self.SLEEP_COUNT_TO_TIMEOUT:
+            raise UpcloudTimeoutException("Timeout, could not destroy node")
+        self._sleep_count += 1
+        self._sleep_func(self.WAIT_AMOUNT)
+
+
+class UpcloudNodeOperations(object):
+
+    def __init__(self, connection):
+        self.connection = connection
+
+    def stop_node(self, node_id):
+        body = {
+            'stop_server': {
+                'stop_type': 'hard'
+            }
+        }
+        self.connection.request('1.2/server/{0}/stop'.format(node_id),
+                                method='POST',
+                                data=json.dumps(body))
+
+    def node_state(self, node_id):
+        action = '1.2/server/{0}'.format(node_id)
+        try:
+            response = self.connection.request(action)
+            return response.object['server']['state']
+        except BaseHTTPError as e:
+            if e.code == 404:
+                return None
+            raise
+
+    def destroy_node(self, node_id):
+        self.connection.request('1.2/server/{0}'.format(node_id),
+                                method='DELETE')
+
+
+class _LoginUser(object):
+
+    def __init__(self, user_id, auth=None):
+        self.user_id = user_id
+        self.auth = auth
+
+    def to_dict(self):
+        login_user = {'username': self.user_id}
+        if self.auth is not None:
+            login_user['ssh_keys'] = {
+                'ssh_key': [self.auth.pubkey]
+            }
+        else:
+            login_user['create_password'] = 'yes'
+
+        return login_user
+
+
+class _StorageDevice(object):
+
+    def __init__(self, image, size):
+        self.image = image
+        self.size = size
+
+    def to_dict(self):
+        extra = self.image.extra
+        if extra['type'] == 'template':
+            return self._storage_device_for_template_image()
+        elif extra['type'] == 'cdrom':
+            return self._storage_device_for_cdrom_image()
+
+    def _storage_device_for_template_image(self):
+        storage_devices = {
+            'storage_device': [{
+                'action': 'clone',
+                'title': self.image.name,
+                'storage': self.image.id
+            }]
+        }
+        return storage_devices
+
+    def _storage_device_for_cdrom_image(self):
+        storage_devices = {
+            'storage_device': [
+                {
+                    'action': 'create',
+                    'title': self.image.name,
+                    'size': self.size.disk,
+                    'tier': self.size.extra['storage_tier']
+
+                },
+                {
+                    'action': 'attach',
+                    'storage': self.image.id,
+                    'type': 'cdrom'
+                }
+            ]
+        }
+        return storage_devices

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/compute/drivers/upcloud.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/upcloud.py b/libcloud/compute/drivers/upcloud.py
new file mode 100644
index 0000000..b9daf84
--- /dev/null
+++ b/libcloud/compute/drivers/upcloud.py
@@ -0,0 +1,277 @@
+# 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.
+"""
+Upcloud node driver
+"""
+import base64
+import json
+
+from libcloud.utils.py3 import httplib, b
+from libcloud.compute.base import NodeDriver, NodeLocation, NodeSize
+from libcloud.compute.base import NodeImage, Node, NodeState
+from libcloud.compute.types import Provider
+from libcloud.common.base import ConnectionUserAndKey, JsonResponse
+from libcloud.common.types import InvalidCredsError
+from libcloud.common.upcloud import UpcloudCreateNodeRequestBody
+from libcloud.common.upcloud import UpcloudNodeDestroyer
+from libcloud.common.upcloud import UpcloudNodeOperations
+
+SERVER_STATE = {
+    'started': NodeState.RUNNING,
+    'stopped': NodeState.STOPPED,
+    'maintenance': NodeState.RECONFIGURING,
+    'error': NodeState.ERROR
+}
+
+
+class UpcloudResponse(JsonResponse):
+    """Response class for UpcloudDriver"""
+
+    def success(self):
+        if self.status == httplib.NO_CONTENT:
+            return True
+        return super(UpcloudResponse, self).success()
+
+    def parse_error(self):
+        data = self.parse_body()
+        if self.status == httplib.UNAUTHORIZED:
+            raise InvalidCredsError(value=data['error']['error_message'])
+        return data
+
+
+class UpcloudConnection(ConnectionUserAndKey):
+    """Connection class for UpcloudDriver"""
+    host = 'api.upcloud.com'
+    responseCls = UpcloudResponse
+
+    def add_default_headers(self, headers):
+        """Adds headers that are needed for all requests"""
+        headers['Authorization'] = self._basic_auth()
+        headers['Accept'] = 'application/json'
+        headers['Content-Type'] = 'application/json'
+        return headers
+
+    def _basic_auth(self):
+        """Constructs basic auth header content string"""
+        credentials = b("{0}:{1}".format(self.user_id, self.key))
+        credentials = base64.b64encode(credentials)
+        return 'Basic {0}'.format(credentials.decode('ascii'))
+
+
+class UpcloudDriver(NodeDriver):
+    """Upcloud node driver
+
+    :keyword    username: Username required for authentication
+    :type       username: ``str``
+
+    :keyword    password: Password required for authentication
+    :type       password: ``str``
+    """
+    type = Provider.UPCLOUD
+    name = 'Upcloud'
+    website = 'https://www.upcloud.com'
+    connectionCls = UpcloudConnection
+    features = {'create_node': ['ssh_key', 'generates_password']}
+
+    def __init__(self, username, password, **kwargs):
+        super(UpcloudDriver, self).__init__(key=username, secret=password,
+                                            **kwargs)
+
+    def list_locations(self):
+        """
+        List available locations for deployment
+
+        :rtype: ``list`` of :class:`NodeLocation`
+        """
+        response = self.connection.request('1.2/zone')
+        return self._to_node_locations(response.object['zones']['zone'])
+
+    def list_sizes(self):
+        """
+        List available plans
+
+        :rtype: ``list`` of :class:`NodeSize`
+        """
+        response = self.connection.request('1.2/plan')
+        return self._to_node_sizes(response.object['plans']['plan'])
+
+    def list_images(self):
+        """
+        List available distributions.
+
+        :rtype: ``list`` of :class:`NodeImage`
+        """
+        response = self.connection.request('1.2/storage/template')
+        obj = response.object
+        response = self.connection.request('1.2/storage/cdrom')
+        storage = response.object['storages']['storage']
+        obj['storages']['storage'].extend(storage)
+        return self._to_node_images(obj['storages']['storage'])
+
+    def create_node(self, name, size, image, location, auth=None, **kwargs):
+        """
+        Creates instance to upcloud.
+
+        If auth is not given then password will be generated.
+
+        :param name:   String with a name for this new node (required)
+        :type name:   ``str``
+
+        :param size:   The size of resources allocated to this node.
+                            (required)
+        :type size:   :class:`.NodeSize`
+
+        :param image:  OS Image to boot on node. (required)
+        :type image:  :class:`.NodeImage`
+
+        :param location: Which data center to create a node in. If empty,
+                              undefined behavior will be selected. (optional)
+        :type location: :class:`.NodeLocation`
+
+        :param auth:   Initial authentication information for the node
+                            (optional)
+        :type auth:   :class:`.NodeAuthSSHKey`
+
+        :return: The newly created node.
+        :rtype: :class:`.Node`
+        """
+        body = UpcloudCreateNodeRequestBody(user_id=self.connection.user_id,
+                                            name=name, size=size, image=image,
+                                            location=location, auth=auth)
+        response = self.connection.request('1.2/server',
+                                           method='POST',
+                                           data=body.to_json())
+        server = response.object['server']
+        # Upcloud server's are in maintenace state when goind
+        # from state to other, it is safe to assume STARTING state
+        return self._to_node(server, state=NodeState.STARTING)
+
+    def list_nodes(self):
+        """
+        List nodes
+
+        :return: List of node objects
+        :rtype: ``list`` of :class:`Node`
+        """
+        servers = []
+        for nid in self._node_ids():
+            response = self.connection.request('1.2/server/{0}'.format(nid))
+            servers.append(response.object['server'])
+        return self._to_nodes(servers)
+
+    def reboot_node(self, node):
+        """
+        Reboot the given node
+
+        :param      node: the node to reboot
+        :type       node: :class:`Node`
+
+        :rtype: ``bool``
+        """
+        body = {
+            'restart_server': {
+                'stop_type': 'hard'
+            }
+        }
+        self.connection.request('1.2/server/{0}/restart'.format(node.id),
+                                method='POST',
+                                data=json.dumps(body))
+        return True
+
+    def destroy_node(self, node):
+        """
+        Destroy the given node
+
+        The disk resources, attached to node,  will not be removed.
+
+        :param       node: the node to destroy
+        :type        node: :class:`Node`
+
+        :rtype: ``bool``
+        """
+
+        operations = UpcloudNodeOperations(self.connection)
+        destroyer = UpcloudNodeDestroyer(operations)
+        return destroyer.destroy_node(node.id)
+
+    def _node_ids(self):
+        """Returns list of server uids currently on upcloud"""
+        response = self.connection.request('1.2/server')
+        servers = response.object['servers']['server']
+        return [server['uuid'] for server in servers]
+
+    def _to_nodes(self, servers):
+        return [self._to_node(server) for server in servers]
+
+    def _to_node(self, server, state=None):
+        ip_addresses = server['ip_addresses']['ip_address']
+        public_ips = [ip['address'] for ip in ip_addresses
+                      if ip['access'] == 'public']
+        private_ips = [ip['address'] for ip in ip_addresses
+                       if ip['access'] == 'private']
+
+        extra = {'vnc_password': server['vnc_password']}
+        if 'password' in server:
+            extra['password'] = server['password']
+        return Node(id=server['uuid'],
+                    name=server['title'],
+                    state=state or SERVER_STATE[server['state']],
+                    public_ips=public_ips,
+                    private_ips=private_ips,
+                    driver=self,
+                    extra=extra)
+
+    def _to_node_locations(self, zones):
+        return [self._construct_node_location(zone) for zone in zones]
+
+    def _construct_node_location(self, zone):
+        return NodeLocation(id=zone['id'],
+                            name=zone['description'],
+                            country=self._parse_country(zone['id']),
+                            driver=self)
+
+    def _parse_country(self, zone_id):
+        """Parses the country information out of zone_id.
+        Zone_id format [country]_[city][number], like fi_hel1"""
+        return zone_id.split('-')[0].upper()
+
+    def _to_node_sizes(self, plans):
+        return [self._construct_node_size(plan) for plan in plans]
+
+    def _construct_node_size(self, plan):
+        extra = self._copy_dict(('core_number', 'storage_tier'), plan)
+        return NodeSize(id=plan['name'], name=plan['name'],
+                        ram=plan['memory_amount'],
+                        disk=plan['storage_size'],
+                        bandwidth=plan['public_traffic_out'],
+                        price=None, driver=self,
+                        extra=extra)
+
+    def _to_node_images(self, images):
+        return [self._construct_node_image(image) for image in images]
+
+    def _construct_node_image(self, image):
+        extra = self._copy_dict(('access', 'license',
+                                 'size', 'state', 'type'), image)
+        return NodeImage(id=image['uuid'],
+                         name=image['title'],
+                         driver=self,
+                         extra=extra)
+
+    def _copy_dict(self, keys, d):
+        extra = {}
+        for key in keys:
+            extra[key] = d[key]
+        return extra

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/compute/types.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py
index deae506..b360c0e 100644
--- a/libcloud/compute/types.py
+++ b/libcloud/compute/types.py
@@ -98,6 +98,7 @@ class Provider(Type):
     :cvar RACKSPACE_FIRST_GEN: Rackspace First Gen Cloud Servers
     :cvar RIMUHOSTING: RimuHosting.com
     :cvar TERREMARK: Terremark
+    :cvar UPCLOUD: Upcloud
     :cvar VCL: VCL driver
     :cvar VCLOUD: vmware vCloud
     :cvar VPSNET: VPS.net
@@ -161,6 +162,7 @@ class Provider(Type):
     SKALICLOUD = 'skalicloud'
     SOFTLAYER = 'softlayer'
     TERREMARK = 'terremark'
+    UPCLOUD = 'upcloud'
     VCL = 'vcl'
     VCLOUD = 'vcloud'
     VOXEL = 'voxel'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/common/test_upcloud.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_upcloud.py b/libcloud/test/common/test_upcloud.py
new file mode 100644
index 0000000..76dd9c1
--- /dev/null
+++ b/libcloud/test/common/test_upcloud.py
@@ -0,0 +1,212 @@
+# 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 json
+
+from mock import Mock, call
+
+from libcloud.common.upcloud import UpcloudCreateNodeRequestBody, UpcloudNodeDestroyer, UpcloudNodeOperations
+from libcloud.common.upcloud import UpcloudTimeoutException
+from libcloud.compute.base import NodeImage, NodeSize, NodeLocation, NodeAuthSSHKey
+from libcloud.test import unittest
+
+
+class TestUpcloudCreateNodeRequestBody(unittest.TestCase):
+
+    def test_creating_node_from_template_image(self):
+        image = NodeImage(id='01000000-0000-4000-8000-000030060200',
+                          name='Ubuntu Server 16.04 LTS (Xenial Xerus)',
+                          driver='',
+                          extra={'type': 'template'})
+        location = NodeLocation(id='fi-hel1', name='Helsinki #1', country='FI', driver='')
+        size = NodeSize(id='1xCPU-1GB', name='1xCPU-1GB', ram=1024, disk=30, bandwidth=2048,
+                        extra={'core_number': 1, 'storage_tier': 'maxiops'}, price=None, driver='')
+
+        body = UpcloudCreateNodeRequestBody(user_id='somename', name='ts', image=image, location=location, size=size)
+        json_body = body.to_json()
+        dict_body = json.loads(json_body)
+        expected_body = {
+            'server': {
+                'title': 'ts',
+                'hostname': 'localhost',
+                'plan': '1xCPU-1GB',
+                'zone': 'fi-hel1',
+                'login_user': {'username': 'somename',
+                               'create_password': 'yes'},
+                'storage_devices': {
+                    'storage_device': [{
+                        'action': 'clone',
+                        'title': 'Ubuntu Server 16.04 LTS (Xenial Xerus)',
+                        'storage': '01000000-0000-4000-8000-000030060200'
+                    }]
+                },
+            }
+        }
+        self.assertDictEqual(expected_body, dict_body)
+
+    def test_creating_node_from_cdrom_image(self):
+        image = NodeImage(id='01000000-0000-4000-8000-000030060200',
+                          name='Ubuntu Server 16.04 LTS (Xenial Xerus)',
+                          driver='',
+                          extra={'type': 'cdrom'})
+        location = NodeLocation(id='fi-hel1', name='Helsinki #1', country='FI', driver='')
+        size = NodeSize(id='1xCPU-1GB', name='1xCPU-1GB', ram=1024, disk=30, bandwidth=2048,
+                        extra={'core_number': 1, 'storage_tier': 'maxiops'}, price=None, driver='')
+
+        body = UpcloudCreateNodeRequestBody(user_id='somename', name='ts', image=image, location=location, size=size)
+        json_body = body.to_json()
+        dict_body = json.loads(json_body)
+        expected_body = {
+            'server': {
+                'title': 'ts',
+                'hostname': 'localhost',
+                'plan': '1xCPU-1GB',
+                'zone': 'fi-hel1',
+                'login_user': {'username': 'somename',
+                               'create_password': 'yes'},
+                'storage_devices': {
+                    'storage_device': [
+                        {
+                            'action': 'create',
+                            'size': 30,
+                            'tier': 'maxiops',
+                            'title': 'Ubuntu Server 16.04 LTS (Xenial Xerus)',
+                        },
+                        {
+                            'action': 'attach',
+                            'storage': '01000000-0000-4000-8000-000030060200',
+                            'type': 'cdrom'
+                        }
+                    ]
+                }
+            }
+        }
+        self.assertDictEqual(expected_body, dict_body)
+
+    def test_creating_node_using_ssh_keys(self):
+        image = NodeImage(id='01000000-0000-4000-8000-000030060200',
+                          name='Ubuntu Server 16.04 LTS (Xenial Xerus)',
+                          driver='',
+                          extra={'type': 'template'})
+        location = NodeLocation(id='fi-hel1', name='Helsinki #1', country='FI', driver='')
+        size = NodeSize(id='1xCPU-1GB', name='1xCPU-1GB', ram=1024, disk=30, bandwidth=2048,
+                        extra={'core_number': 1, 'storage_tier': 'maxiops'}, price=None, driver='')
+        auth = NodeAuthSSHKey('sshkey')
+
+        body = UpcloudCreateNodeRequestBody(user_id='somename', name='ts', image=image, location=location, size=size, auth=auth)
+        json_body = body.to_json()
+        dict_body = json.loads(json_body)
+        expected_body = {
+            'server': {
+                'title': 'ts',
+                'hostname': 'localhost',
+                'plan': '1xCPU-1GB',
+                'zone': 'fi-hel1',
+                'login_user': {
+                    'username': 'somename',
+                    'ssh_keys': {
+                        'ssh_key': [
+                            'sshkey'
+                        ]
+                    },
+                },
+                'storage_devices': {
+                    'storage_device': [{
+                        'action': 'clone',
+                        'title': 'Ubuntu Server 16.04 LTS (Xenial Xerus)',
+                        'storage': '01000000-0000-4000-8000-000030060200'
+                    }]
+                },
+            }
+        }
+        self.assertDictEqual(expected_body, dict_body)
+
+
+class TestUpcloudNodeDestroyer(unittest.TestCase):
+
+    def setUp(self):
+        self.mock_sleep = Mock()
+        self.mock_operations = Mock(spec=UpcloudNodeOperations)
+        self.destroyer = UpcloudNodeDestroyer(self.mock_operations, sleep_func=self.mock_sleep)
+
+    def test_node_already_in_stopped_state(self):
+        self.mock_operations.node_state.side_effect = ['stopped']
+
+        self.assertTrue(self.destroyer.destroy_node(1))
+
+        self.assertTrue(self.mock_operations.stop_node.call_count == 0)
+        self.mock_operations.destroy_node.assert_called_once_with(1)
+
+    def test_node_in_error_state(self):
+        self.mock_operations.node_state.side_effect = ['error']
+
+        self.assertFalse(self.destroyer.destroy_node(1))
+
+        self.assertTrue(self.mock_operations.stop_node.call_count == 0)
+        self.assertTrue(self.mock_operations.destroy_node.call_count == 0)
+
+    def test_node_in_started_state(self):
+        self.mock_operations.node_state.side_effect = ['started', 'stopped']
+
+        self.assertTrue(self.destroyer.destroy_node(1))
+
+        self.mock_operations.stop_node.assert_called_once_with(1)
+        self.mock_operations.destroy_node.assert_called_once_with(1)
+
+    def test_node_in_maintenace_state(self):
+        self.mock_operations.node_state.side_effect = ['maintenance', 'maintenance', None]
+
+        self.assertTrue(self.destroyer.destroy_node(1))
+
+        self.mock_sleep.assert_has_calls([call(self.destroyer.WAIT_AMOUNT), call(self.destroyer.WAIT_AMOUNT)])
+
+        self.assertTrue(self.mock_operations.stop_node.call_count == 0)
+        self.assertTrue(self.mock_operations.destroy_node.call_count == 0)
+
+    def test_node_statys_in_started_state_for_awhile(self):
+        self.mock_operations.node_state.side_effect = ['started', 'started', 'stopped']
+
+        self.assertTrue(self.destroyer.destroy_node(1))
+
+        # Only one all for stop should be done
+        self.mock_operations.stop_node.assert_called_once_with(1)
+        self.mock_sleep.assert_has_calls([call(self.destroyer.WAIT_AMOUNT)])
+        self.mock_operations.destroy_node.assert_called_once_with(1)
+
+    def test_reuse(self):
+        "Verify that internal flag self.destroyer._stop_node is handled properly"
+        self.mock_operations.node_state.side_effect = ['started', 'stopped', 'started', 'stopped']
+        self.assertTrue(self.destroyer.destroy_node(1))
+        self.assertTrue(self.destroyer.destroy_node(1))
+
+        self.assertEquals(self.mock_sleep.call_count, 0)
+        self.assertEquals(self.mock_operations.stop_node.call_count, 2)
+
+    def test_timeout(self):
+        self.mock_operations.node_state.side_effect = ['maintenance'] * 50
+
+        self.assertRaises(UpcloudTimeoutException, self.destroyer.destroy_node, 1)
+
+    def test_timeout_reuse(self):
+        "Verify sleep count is handled properly"
+        self.mock_operations.node_state.side_effect = ['maintenance'] * 50
+        self.assertRaises(UpcloudTimeoutException, self.destroyer.destroy_node, 1)
+
+        self.mock_operations.node_state.side_effect = ['maintenance', None]
+        self.assertTrue(self.destroyer.destroy_node(1))
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_plan.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_plan.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_plan.json
new file mode 100644
index 0000000..c59dcf0
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_plan.json
@@ -0,0 +1,38 @@
+{
+   "plans" : {
+      "plan" : [
+         {
+            "core_number" : 1,
+            "memory_amount" : 1024,
+            "name" : "1xCPU-1GB",
+            "public_traffic_out" : 2048,
+            "storage_size" : 30,
+            "storage_tier" : "maxiops"
+         },
+         {
+            "core_number" : 2,
+            "memory_amount" : 2048,
+            "name" : "2xCPU-2GB",
+            "public_traffic_out" : 3072,
+            "storage_size" : 50,
+            "storage_tier" : "maxiops"
+         },
+         {
+            "core_number" : 4,
+            "memory_amount" : 4096,
+            "name" : "4xCPU-4GB",
+            "public_traffic_out" : 4096,
+            "storage_size" : 100,
+            "storage_tier" : "maxiops"
+         },
+         {
+            "core_number" : 6,
+            "memory_amount" : 8192,
+            "name" : "6xCPU-8GB",
+            "public_traffic_out" : 8192,
+            "storage_size" : 200,
+            "storage_tier" : "maxiops"
+         }
+      ]
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_server.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_server.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_server.json
new file mode 100644
index 0000000..e2a7d0a
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_server.json
@@ -0,0 +1,22 @@
+{
+   "servers" : {
+      "server" : [
+         {
+            "core_number" : "1",
+            "hostname" : "localhost",
+            "license" : 0,
+            "memory_amount" : "1024",
+            "plan" : "1xCPU-1GB",
+            "plan_ipv4_bytes" : "12267",
+            "plan_ipv6_bytes" : "4644",
+            "state" : "started",
+            "tags" : {
+               "tag" : []
+            },
+            "title" : "test_server",
+            "uuid" : "00f8c525-7e62-4108-8115-3958df5b43dc",
+            "zone" : "fi-hel1"
+         }
+      ]
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00893c98-5d5a-4363-b177-88df518a2b60.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00893c98-5d5a-4363-b177-88df518a2b60.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00893c98-5d5a-4363-b177-88df518a2b60.json
new file mode 100644
index 0000000..c99fc05
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00893c98-5d5a-4363-b177-88df518a2b60.json
@@ -0,0 +1,58 @@
+{
+   "server" : {
+      "boot_order" : "cdrom,disk",
+      "core_number" : "1",
+      "firewall" : "off",
+      "host" : 4297867907,
+      "hostname" : "localhost",
+      "ip_addresses" : {
+         "ip_address" : [
+            {
+               "access" : "private",
+               "address" : "10.2.1.244",
+               "family" : "IPv4"
+            },
+            {
+               "access" : "public",
+               "address" : "2a04:3541:1000:500:7cae:1dff:fead:5bde",
+               "family" : "IPv6"
+            },
+            {
+               "access" : "public",
+               "address" : "83.136.254.34",
+               "family" : "IPv4",
+               "part_of_plan" : "yes"
+            }
+         ]
+      },
+      "license" : 0,
+      "memory_amount" : "1024",
+      "nic_model" : "virtio",
+      "plan" : "1xCPU-1GB",
+      "plan_ipv4_bytes" : "0",
+      "plan_ipv6_bytes" : "0",
+      "state" : "stopped",
+      "storage_devices" : {
+         "storage_device" : [
+            {
+               "address" : "virtio:0",
+               "part_of_plan" : "yes",
+               "storage" : "01839922-a675-4214-b10e-a4cf7953992f",
+               "storage_size" : 30,
+               "storage_title" : "localhost-disk0",
+               "type" : "disk"
+            }
+         ]
+      },
+      "tags" : {
+         "tag" : []
+      },
+      "timezone" : "UTC",
+      "title" : "test",
+      "uuid" : "00893c98-5d5a-4363-b177-88df518a2b60",
+      "video_model" : "cirrus",
+      "vnc" : "off",
+      "vnc_password" : "Hf7qpJs8",
+      "zone" : "uk-lon1"
+   }
+}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc.json
new file mode 100644
index 0000000..1278e6e
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc.json
@@ -0,0 +1,57 @@
+{
+   "server" : {
+      "boot_order" : "disk",
+      "core_number" : "1",
+      "firewall" : "off",
+      "host" : 4964762243,
+      "hostname" : "localhost",
+      "ip_addresses" : {
+         "ip_address" : [
+            {
+               "access" : "private",
+               "address" : "10.1.7.68",
+               "family" : "IPv4"
+            },
+            {
+               "access" : "public",
+               "address" : "2a04:3540:1000:310:7cae:1dff:fead:19dc",
+               "family" : "IPv6"
+            },
+            {
+               "access" : "public",
+               "address" : "94.237.37.249",
+               "family" : "IPv4",
+               "part_of_plan" : "yes"
+            }
+         ]
+      },
+      "license" : 0,
+      "memory_amount" : "1024",
+      "nic_model" : "virtio",
+      "plan" : "1xCPU-1GB",
+      "plan_ipv4_bytes" : "8242",
+      "plan_ipv6_bytes" : "3440",
+      "state" : "started",
+      "storage_devices" : {
+         "storage_device" : [
+            {
+               "address" : "virtio:0",
+               "storage" : "01e5411f-37c9-4b8e-b0bb-4cad5119b3ea",
+               "storage_size" : 10,
+               "storage_title" : "Ubuntu Server 16.04 LTS (Xenial Xerus)",
+               "type" : "disk"
+            }
+         ]
+      },
+      "tags" : {
+         "tag" : []
+      },
+      "timezone" : "UTC",
+      "title" : "test_server",
+      "uuid" : "00f8c525-7e62-4108-8115-3958df5b43dc",
+      "video_model" : "cirrus",
+      "vnc" : "off",
+      "vnc_password" : "r362XMmV",
+      "zone" : "fi-hel1"
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc_restart.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc_restart.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc_restart.json
new file mode 100644
index 0000000..af904e4
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc_restart.json
@@ -0,0 +1,57 @@
+{
+   "server" : {
+      "boot_order" : "disk",
+      "core_number" : "1",
+      "firewall" : "off",
+      "host" : 4964762243,
+      "hostname" : "localhost",
+      "ip_addresses" : {
+         "ip_address" : [
+            {
+               "access" : "private",
+               "address" : "10.1.7.68",
+               "family" : "IPv4"
+            },
+            {
+               "access" : "public",
+               "address" : "2a04:3540:1000:310:7cae:1dff:fead:19dc",
+               "family" : "IPv6"
+            },
+            {
+               "access" : "public",
+               "address" : "94.237.37.249",
+               "family" : "IPv4",
+               "part_of_plan" : "yes"
+            }
+         ]
+      },
+      "license" : 0,
+      "memory_amount" : "1024",
+      "nic_model" : "virtio",
+      "plan" : "1xCPU-1GB",
+      "plan_ipv4_bytes" : "218066",
+      "plan_ipv6_bytes" : "6450",
+      "state" : "started",
+      "storage_devices" : {
+         "storage_device" : [
+            {
+               "address" : "virtio:0",
+               "storage" : "01e5411f-37c9-4b8e-b0bb-4cad5119b3ea",
+               "storage_size" : 10,
+               "storage_title" : "Ubuntu Server 16.04 LTS (Xenial Xerus)",
+               "type" : "disk"
+            }
+         ]
+      },
+      "tags" : {
+         "tag" : []
+      },
+      "timezone" : "UTC",
+      "title" : "test_server",
+      "uuid" : "00f8c525-7e62-4108-8115-3958df5b43dc",
+      "video_model" : "cirrus",
+      "vnc" : "off",
+      "vnc_password" : "r362XMmV",
+      "zone" : "fi-hel1"
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_server_from_cdrom.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_server_from_cdrom.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_from_cdrom.json
new file mode 100644
index 0000000..b82f8c6
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_from_cdrom.json
@@ -0,0 +1,65 @@
+{
+   "server" : {
+      "boot_order" : "disk",
+      "core_number" : "1",
+      "firewall" : "off",
+      "hostname" : "localhost",
+      "ip_addresses" : {
+         "ip_address" : [
+            {
+               "access" : "private",
+               "address" : "10.1.2.26",
+               "family" : "IPv4"
+            },
+            {
+               "access" : "public",
+               "address" : "2a04:3540:1000:310:7cae:1dff:fead:5683",
+               "family" : "IPv6"
+            },
+            {
+               "access" : "public",
+               "address" : "94.237.37.215",
+               "family" : "IPv4",
+               "part_of_plan" : "yes"
+            }
+         ]
+      },
+      "license" : 0,
+      "memory_amount" : "1024",
+      "nic_model" : "virtio",
+      "plan" : "1xCPU-1GB",
+      "plan_ipv4_bytes" : "0",
+      "plan_ipv6_bytes" : "0",
+      "progress" : "0",
+      "state" : "maintenance",
+      "storage_devices" : {
+         "storage_device" : [
+            {
+               "address" : "virtio:0",
+               "part_of_plan" : "yes",
+               "storage" : "01898fe2-9909-471e-b33c-59d7896b48f5",
+               "storage_size" : 30,
+               "storage_title" : "Ubuntu Server 16.04 LTS (Xenial Xerus), 64-bit",
+               "type" : "disk"
+            },
+            {
+               "address" : "ide:0:0",
+               "storage" : "01000000-0000-4000-8000-000030040101",
+               "storage_size" : 1,
+               "storage_title" : "Ubuntu Server 14.04 LTS",
+               "type" : "cdrom"
+            }
+         ]
+      },
+      "tags" : {
+         "tag" : []
+      },
+      "timezone" : "UTC",
+      "title" : "test_server",
+      "uuid" : "00c68ee6-e6e1-4d5f-a213-4a1e063a3cbd",
+      "video_model" : "cirrus",
+      "vnc" : "off",
+      "vnc_password" : "5C4TVPf8",
+      "zone" : "fi-hel1"
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_server_from_template.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_server_from_template.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_from_template.json
new file mode 100644
index 0000000..cde80cc
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_server_from_template.json
@@ -0,0 +1,59 @@
+{
+   "server" : {
+      "boot_order" : "disk",
+      "core_number" : "1",
+      "firewall" : "off",
+      "hostname" : "localhost",
+      "ip_addresses" : {
+         "ip_address" : [
+            {
+               "access" : "private",
+               "address" : "10.1.3.163",
+               "family" : "IPv4"
+            },
+            {
+               "access" : "public",
+               "address" : "2a04:3540:1000:310:7cae:1dff:fead:3332",
+               "family" : "IPv6"
+            },
+            {
+               "access" : "public",
+               "address" : "94.237.37.216",
+               "family" : "IPv4",
+               "part_of_plan" : "yes"
+            }
+         ]
+      },
+      "license" : 0,
+      "memory_amount" : "1024",
+      "nic_model" : "virtio",
+      "password" : "777gznbm",
+      "plan" : "1xCPU-1GB",
+      "plan_ipv4_bytes" : "0",
+      "plan_ipv6_bytes" : "0",
+      "progress" : "0",
+      "state" : "maintenance",
+      "storage_devices" : {
+         "storage_device" : [
+            {
+               "address" : "virtio:0",
+               "storage" : "018e2c82-1c16-46b9-8b7d-aeaf8d8309a9",
+               "storage_size" : 10,
+               "storage_title" : "Ubuntu Server 16.04 LTS (Xenial Xerus)",
+               "type" : "disk"
+            }
+         ]
+      },
+      "tags" : {
+         "tag" : []
+      },
+      "timezone" : "UTC",
+      "title" : "test_server",
+      "username" : "mlackman",
+      "uuid" : "00814aac-240f-4f08-9139-9697c9ffc0b7",
+      "video_model" : "cirrus",
+      "vnc" : "off",
+      "vnc_password" : "G4FvMjvg",
+      "zone" : "fi-hel1"
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_cdrom.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_cdrom.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_cdrom.json
new file mode 100644
index 0000000..5f45a2b
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_cdrom.json
@@ -0,0 +1,411 @@
+{
+   "storages" : {
+      "storage" : [
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Standard (CD 1)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010010101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Standard (CD 2)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010010102"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Standard (CD 1)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010010201"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Standard (CD 2)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010010202"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Enterprise (CD 1)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010020101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Enterprise (CD 2)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010020102"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Enterprise (CD 1)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010020201"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Enterprise (CD 2)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010020202"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 3,
+            "state" : "online",
+            "title" : "Windows Server 2008 R2 Datacenter",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010030101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Datacenter",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010040101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Windows Server 2003 R2 Datacenter",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010040201"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 4,
+            "state" : "online",
+            "title" : "Windows Server 2012",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000010050101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 5,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 6.0.1 (Squeeze) (DVD)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000020010101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 6.0.1 (Squeeze) (netinst)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000020010201"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 6.0.1 (Squeeze) (netinst)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000020010301"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 6.0.1 (Squeeze) (live)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000020010401"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 6.0.1 (Squeeze) (live)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000020010501"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 7.8 (Wheezy)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000020020201"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 8.6.0 (Jessie)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000020030101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 9.0.0 (Stretch) Installation CD",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000020040101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Ubuntu Server 10.04",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000030010101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Ubuntu Server 10.04",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000030010201"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Ubuntu Server 11.04",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000030020101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Ubuntu Server 11.04",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000030020201"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Ubuntu Server 12.04",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000030030101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Ubuntu Server 14.04 LTS",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000030040101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Ubuntu Server 16.04 LTS (Xenial Xerus), 64-bit",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000030060101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 4,
+            "state" : "online",
+            "title" : "Fedora 16 (DVD)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000040010101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Fedora 16 (live)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000040010201"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Fedora 19",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000040020101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 4,
+            "state" : "online",
+            "title" : "CentOS 6.0 (DVD 1)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000050010101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "CentOS 6.0 (DVD 2)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000050010102"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 4,
+            "state" : "online",
+            "title" : "CentOS 6.8 (DVD 1)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000050010103"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 3,
+            "state" : "online",
+            "title" : "CentOS 6.8 (DVD 2)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000050010104"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 4,
+            "state" : "online",
+            "title" : "CentOS 6.8 (DVD 1)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000050010105"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "CentOS 6.8 (DVD 2)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000050010106"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 5,
+            "state" : "online",
+            "title" : "CentOS 7.3-1611 DVD",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000050010301"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 4,
+            "state" : "online",
+            "title" : "Knoppix 6.4.4",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000060010101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Arch Linux 2010.05",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000070010101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "CoreOS 607.0.0",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000080010101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "CoreOS 845.0.0",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000080010201"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "CoreOS Alpha (1032.1.0)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000080010501"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "CoreOS Stable (1068.8.0)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000080020101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "Gentoo Linux Minimal Installation CD (64bit)",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000090010101"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 1,
+            "state" : "online",
+            "title" : "FreeBSD 11.0-RELEASE amd64 Installation CD",
+            "type" : "cdrom",
+            "uuid" : "01000000-0000-4000-8000-000100010101"
+         }
+      ]
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_template.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_template.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_template.json
new file mode 100644
index 0000000..12f42c2
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_storage_template.json
@@ -0,0 +1,114 @@
+{
+   "storages" : {
+      "storage" : [
+         {
+            "access" : "public",
+            "license" : 3.36,
+            "size" : 20,
+            "state" : "online",
+            "title" : "Windows Server 2012 R2 Datacenter",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000010050200"
+         },
+         {
+            "access" : "public",
+            "license" : 0.694,
+            "size" : 20,
+            "state" : "online",
+            "title" : "Windows Server 2012 R2 Standard",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000010050300"
+         },
+         {
+            "access" : "public",
+            "license" : 3.36,
+            "size" : 20,
+            "state" : "online",
+            "title" : "Windows Server 2016 Datacenter",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000010060200"
+         },
+         {
+            "access" : "public",
+            "license" : 0.694,
+            "size" : 20,
+            "state" : "online",
+            "title" : "Windows Server 2016 Standard",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000010060300"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 7.8 (Wheezy)",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000020020100"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "Debian GNU/Linux 8.7 (Jessie)",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000020030100"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "Ubuntu Server 12.04 LTS (Precise Pangolin)",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000030030200"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "Ubuntu Server 14.04 LTS (Trusty Tahr)",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000030040200"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "Ubuntu Server 16.04 LTS (Xenial Xerus)",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000030060200"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "CentOS 6.9",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000050010200"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 2,
+            "state" : "online",
+            "title" : "CentOS 7.0",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000050010300"
+         },
+         {
+            "access" : "public",
+            "license" : 0,
+            "size" : 5,
+            "state" : "online",
+            "title" : "CoreOS Stable 1068.8.0",
+            "type" : "template",
+            "uuid" : "01000000-0000-4000-8000-000080010200"
+         }
+      ]
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_zone.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_zone.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_zone.json
new file mode 100644
index 0000000..19deb8d
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_zone.json
@@ -0,0 +1,30 @@
+{
+   "zones" : {
+      "zone" : [
+         {
+            "description" : "Frankfurt #1",
+            "id" : "de-fra1"
+         },
+         {
+            "description" : "Helsinki #1",
+            "id" : "fi-hel1"
+         },
+         {
+            "description" : "Amsterdam #1",
+            "id" : "nl-ams1"
+         },
+         {
+            "description" : "Singapore #1",
+            "id" : "sg-sin1"
+         },
+         {
+            "description" : "London #1",
+            "id" : "uk-lon1"
+         },
+         {
+            "description" : "Chicago #1",
+            "id" : "us-chi1"
+         }
+      ]
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/fixtures/upcloud/api_1_2_zone_failed_auth.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/upcloud/api_1_2_zone_failed_auth.json b/libcloud/test/compute/fixtures/upcloud/api_1_2_zone_failed_auth.json
new file mode 100644
index 0000000..356841c
--- /dev/null
+++ b/libcloud/test/compute/fixtures/upcloud/api_1_2_zone_failed_auth.json
@@ -0,0 +1,6 @@
+{
+   "error" : {
+      "error_code" : "AUTHENTICATION_FAILED",
+      "error_message" : "Authentication failed using the given username and password."
+   }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/abbd9bb6/libcloud/test/compute/test_upcloud.py
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/test_upcloud.py b/libcloud/test/compute/test_upcloud.py
new file mode 100644
index 0000000..41d02a0
--- /dev/null
+++ b/libcloud/test/compute/test_upcloud.py
@@ -0,0 +1,248 @@
+# 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.
+
+from __future__ import with_statement
+import sys
+import re
+import json
+import base64
+
+from libcloud.utils.py3 import httplib, ensure_string
+from libcloud.compute.drivers.upcloud import UpcloudDriver
+from libcloud.common.types import InvalidCredsError
+from libcloud.compute.drivers.upcloud import UpcloudResponse
+from libcloud.compute.types import NodeState
+from libcloud.compute.base import NodeImage, NodeSize, NodeLocation, NodeAuthSSHKey, Node
+from libcloud.test import LibcloudTestCase, unittest, MockHttp
+from libcloud.test.file_fixtures import ComputeFileFixtures
+from libcloud.test.secrets import UPCLOUD_PARAMS
+
+
+class UpcloudPersistResponse(UpcloudResponse):
+
+    def parse_body(self):
+        import os
+        path = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.pardir, 'compute', 'fixtures', 'upcloud'))
+        filename = 'api' + self.request.path_url.replace('/', '_').replace('.', '_') + '.json'
+        filename = os.path.join(path, filename)
+        if not os.path.exists(filename):
+            with open(filename, 'w+') as f:
+                f.write(self.body)
+        return super(UpcloudPersistResponse, self).parse_body()
+
+
+class UpcloudAuthenticationTests(LibcloudTestCase):
+
+    def setUp(self):
+        UpcloudDriver.connectionCls.conn_class = UpcloudMockHttp
+        self.driver = UpcloudDriver("nosuchuser", "nopwd")
+
+    def test_authentication_fails(self):
+        with self.assertRaises(InvalidCredsError):
+            self.driver.list_locations()
+
+
+class UpcloudDriverTests(LibcloudTestCase):
+
+    def setUp(self):
+        UpcloudDriver.connectionCls.conn_class = UpcloudMockHttp
+        # UpcloudDriver.connectionCls.responseCls = UpcloudPersistResponse
+        self.driver = UpcloudDriver(*UPCLOUD_PARAMS)
+
+    def test_features(self):
+        features = self.driver.features['create_node']
+        self.assertIn('ssh_key', features)
+        self.assertIn('generates_password', features)
+
+    def test_list_locations(self):
+        locations = self.driver.list_locations()
+        self.assertTrue(len(locations) >= 1)
+        expected_node_location = NodeLocation(id='fi-hel1',
+                                              name='Helsinki #1',
+                                              country='FI',
+                                              driver=self.driver)
+        self.assert_object(expected_node_location, objects=locations)
+
+    def test_list_sizes(self):
+        sizes = self.driver.list_sizes()
+        self.assertTrue(len(sizes) >= 1)
+        expected_node_size = NodeSize(id='1xCPU-1GB',
+                                      name='1xCPU-1GB',
+                                      ram=1024,
+                                      disk=30,
+                                      bandwidth=2048,
+                                      price=None,
+                                      driver=self.driver,
+                                      extra={'core_number': 1,
+                                             'storage_tier': 'maxiops'})
+        self.assert_object(expected_node_size, objects=sizes)
+
+    def test_list_images(self):
+        images = self.driver.list_images()
+        self.assertTrue(len(images) >= 1)
+        expected_node_image = NodeImage(id='01000000-0000-4000-8000-000010010101',
+                                        name='Windows Server 2003 R2 Standard (CD 1)',
+                                        driver=self.driver,
+                                        extra={'access': 'public',
+                                               'licence': 0,
+                                               'size': 1,
+                                               'state': 'online',
+                                               'type': 'cdrom'})
+        self.assert_object(expected_node_image, objects=images)
+
+    def test_create_node_from_template(self):
+        image = NodeImage(id='01000000-0000-4000-8000-000030060200',
+                          name='Ubuntu Server 16.04 LTS (Xenial Xerus)',
+                          extra={'type': 'template'},
+                          driver=self.driver)
+        location = NodeLocation(id='fi-hel1', name='Helsinki #1', country='FI', driver=self.driver)
+        size = NodeSize(id='1xCPU-1GB', name='1xCPU-1GB', ram=1024, disk=30, bandwidth=2048,
+                        extra={'core_number': 1, 'storage_tier': 'maxiops'}, price=None, driver=self.driver)
+        node = self.driver.create_node(name='test_server', size=size, image=image, location=location)
+
+        self.assertTrue(re.match('^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$', node.id))
+        self.assertEquals(node.name, 'test_server')
+        self.assertEquals(node.state, NodeState.STARTING)
+        self.assertTrue(len(node.public_ips) > 0)
+        self.assertTrue(len(node.private_ips) > 0)
+        self.assertEquals(node.driver, self.driver)
+        self.assertTrue(len(node.extra['password']) > 0)
+        self.assertTrue(len(node.extra['vnc_password']) > 0)
+
+    def test_create_node_with_ssh_keys(self):
+        image = NodeImage(id='01000000-0000-4000-8000-000030060200',
+                          name='Ubuntu Server 16.04 LTS (Xenial Xerus)',
+                          extra={'type': 'template'},
+                          driver=self.driver)
+        location = NodeLocation(id='fi-hel1', name='Helsinki #1', country='FI', driver=self.driver)
+        size = NodeSize(id='1xCPU-1GB', name='1xCPU-1GB', ram=1024, disk=30, bandwidth=2048,
+                        extra={'core_number': 1, 'storage_tier': 'maxiops'}, price=None, driver=self.driver)
+
+        auth = NodeAuthSSHKey('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCUUFfYA+T+BzoM7IIR' +
+                              'VXNndDjYvIROMjfyRBhhHf6RZd1IkAwcWSGISePh2tIiqu8gJalYYHg2w' +
+                              'i3ofMJfi6VYeyBFWrIDhMK0v+ziBbBUtlJNnP6MBOR/13avkk+76TVrcG' +
+                              'xu49RaptYNzZ21XluvIlaqqdjAhoh0J+o7OZTKD7N1UTPL7CIX+ITaA+g' +
+                              '3FR5ITClk8KmIbp3vT6fUPD7pNUrGBZTpcPcHq8rodQ8igWIVdSkb9iky' +
+                              'ew4y6wvsubQ3Ykn26XeKxrk1vA6ZKMHt7ijCYmfL0LcDfctNymy/vc6hs' +
+                              'WxCRS5OqNQ6nxdXpv9A+TD0sJuf5jaoH7MSpU1 mika.lackman@gmail.com')
+        node = self.driver.create_node(name='test_server', size=size, image=image, location=location, auth=auth)
+        self.assertTrue(re.match('^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$', node.id))
+        self.assertEquals(node.name, 'test_server')
+        self.assertEquals(node.state, NodeState.STARTING)
+        self.assertTrue(len(node.public_ips) > 0)
+        self.assertTrue(len(node.private_ips) > 0)
+        self.assertEquals(node.driver, self.driver)
+
+    def test_list_nodes(self):
+        nodes = self.driver.list_nodes()
+
+        self.assertTrue(len(nodes) >= 1)
+        node = nodes[0]
+        self.assertEquals(node.name, 'test_server')
+        self.assertEquals(node.state, NodeState.RUNNING)
+        self.assertTrue(len(node.public_ips) > 0)
+        self.assertTrue(len(node.private_ips) > 0)
+        self.assertEquals(node.driver, self.driver)
+
+    def test_reboot_node(self):
+        nodes = self.driver.list_nodes()
+        success = self.driver.reboot_node(nodes[0])
+        self.assertTrue(success)
+
+    def test_destroy_node(self):
+        if UpcloudDriver.connectionCls.conn_class == UpcloudMockHttp:
+            nodes = [Node(id='00893c98_5d5a_4363_b177_88df518a2b60', name='', state='',
+                          public_ips=[], private_ips=[], driver=self.driver)]
+        else:
+            nodes = self.driver.list_nodes()
+        success = self.driver.destroy_node(nodes[0])
+        self.assertTrue(success)
+
+    def assert_object(self, expected_object, objects):
+        same_data = any([self.objects_equals(expected_object, obj) for obj in objects])
+        self.assertTrue(same_data, "Objects does not match")
+
+    def objects_equals(self, expected_obj, obj):
+        for name in vars(expected_obj):
+            expected_data = getattr(expected_obj, name)
+            actual_data = getattr(obj, name)
+            same_data = self.data_equals(expected_data, actual_data)
+            if not same_data:
+                break
+        return same_data
+
+    def data_equals(self, expected_data, actual_data):
+        if isinstance(expected_data, dict):
+            return self.dicts_equals(expected_data, actual_data)
+        else:
+            return expected_data == actual_data
+
+    def dicts_equals(self, d1, d2):
+        """Assumes dicts to contain only hashable types"""
+        return set(d1.values()) == set(d2.values())
+
+
+class UpcloudMockHttp(MockHttp):
+    fixtures = ComputeFileFixtures('upcloud')
+
+    def _1_2_zone(self, method, url, body, headers):
+        auth = headers['Authorization'].split(' ')[1]
+        username, password = ensure_string(base64.b64decode(auth)).split(':')
+        if username == 'nosuchuser' and password == 'nopwd':
+            body = self.fixtures.load('api_1_2_zone_failed_auth.json')
+            status_code = httplib.UNAUTHORIZED
+        else:
+            body = self.fixtures.load('api_1_2_zone.json')
+            status_code = httplib.OK
+        return (status_code, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_plan(self, method, url, body, headers):
+        body = self.fixtures.load('api_1_2_plan.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_storage_cdrom(self, method, url, body, headers):
+        body = self.fixtures.load('api_1_2_storage_cdrom.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_storage_template(self, method, url, body, headers):
+        body = self.fixtures.load('api_1_2_storage_template.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_server(self, method, url, body, headers):
+        if method == 'POST':
+            dbody = json.loads(body)
+            storages = dbody['server']['storage_devices']['storage_device']
+            if any(['type' in storage and storage['type'] == 'cdrom' for storage in storages]):
+                body = self.fixtures.load('api_1_2_server_from_cdrom.json')
+            else:
+                body = self.fixtures.load('api_1_2_server_from_template.json')
+        else:
+            body = self.fixtures.load('api_1_2_server.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_server_00f8c525_7e62_4108_8115_3958df5b43dc(self, method, url, body, headers):
+        body = self.fixtures.load('api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_server_00f8c525_7e62_4108_8115_3958df5b43dc_restart(self, method, url, body, headers):
+        body = self.fixtures.load('api_1_2_server_00f8c525-7e62-4108-8115-3958df5b43dc_restart.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _1_2_server_00893c98_5d5a_4363_b177_88df518a2b60(self, method, url, body, headers):
+        body = self.fixtures.load('api_1_2_server_00893c98-5d5a-4363-b177-88df518a2b60.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/abbd9bb6/libcloud/test/secrets.py-dist
----------------------------------------------------------------------
diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist
index 1b30bf2..8eaa0c6 100644
--- a/libcloud/test/secrets.py-dist
+++ b/libcloud/test/secrets.py-dist
@@ -57,6 +57,7 @@ VULTR_PARAMS = ('key')
 PACKET_PARAMS = ('api_key')
 ECS_PARAMS = ('access_key', 'access_secret')
 CLOUDSCALE_PARAMS = ('token',)
+UPCLOUD_PARAMS = ('user', 'secret')
 
 # Storage
 STORAGE_S3_PARAMS = ('key', 'secret')