338 lines
12 KiB
Python
338 lines
12 KiB
Python
# 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.
|
|
"""
|
|
RimuHosting Driver
|
|
"""
|
|
try:
|
|
import simplejson as json
|
|
except ImportError:
|
|
import json
|
|
|
|
from libcloud.common.base import ConnectionKey, JsonResponse
|
|
from libcloud.common.types import InvalidCredsError
|
|
from libcloud.compute.types import Provider, NodeState
|
|
from libcloud.compute.base import NodeDriver, NodeSize, Node, NodeLocation
|
|
from libcloud.compute.base import NodeImage
|
|
|
|
API_CONTEXT = '/r'
|
|
API_HOST = 'rimuhosting.com'
|
|
|
|
|
|
class RimuHostingException(Exception):
|
|
"""
|
|
Exception class for RimuHosting driver
|
|
"""
|
|
|
|
def __str__(self):
|
|
# pylint: disable=unsubscriptable-object
|
|
return self.args[0]
|
|
|
|
def __repr__(self):
|
|
# pylint: disable=unsubscriptable-object
|
|
return "<RimuHostingException '%s'>" % (self.args[0])
|
|
|
|
|
|
class RimuHostingResponse(JsonResponse):
|
|
"""
|
|
Response Class for RimuHosting driver
|
|
"""
|
|
def success(self):
|
|
if self.status == 403:
|
|
raise InvalidCredsError()
|
|
return True
|
|
|
|
def parse_body(self):
|
|
try:
|
|
js = super(RimuHostingResponse, self).parse_body()
|
|
keys = list(js.keys())
|
|
if js[keys[0]]['response_type'] == "ERROR":
|
|
raise RimuHostingException(
|
|
js[keys[0]]['human_readable_message']
|
|
)
|
|
return js[keys[0]]
|
|
except KeyError:
|
|
raise RimuHostingException('Could not parse body: %s'
|
|
% (self.body))
|
|
|
|
|
|
class RimuHostingConnection(ConnectionKey):
|
|
"""
|
|
Connection class for the RimuHosting driver
|
|
"""
|
|
|
|
api_context = API_CONTEXT
|
|
host = API_HOST
|
|
port = 443
|
|
responseCls = RimuHostingResponse
|
|
|
|
def __init__(self, key, secure=True, retry_delay=None,
|
|
backoff=None, timeout=None):
|
|
# override __init__ so that we can set secure of False for testing
|
|
ConnectionKey.__init__(self, key, secure, timeout=timeout,
|
|
retry_delay=retry_delay, backoff=backoff)
|
|
|
|
def add_default_headers(self, headers):
|
|
# We want JSON back from the server. Could be application/xml
|
|
# (but JSON is better).
|
|
headers['Accept'] = 'application/json'
|
|
# Must encode all data as json, or override this header.
|
|
headers['Content-Type'] = 'application/json'
|
|
|
|
headers['Authorization'] = 'rimuhosting apikey=%s' % (self.key)
|
|
return headers
|
|
|
|
def request(self, action, params=None, data='', headers=None,
|
|
method='GET'):
|
|
if not headers:
|
|
headers = {}
|
|
if not params:
|
|
params = {}
|
|
# Override this method to prepend the api_context
|
|
return ConnectionKey.request(self, self.api_context + action,
|
|
params, data, headers, method)
|
|
|
|
|
|
class RimuHostingNodeDriver(NodeDriver):
|
|
"""
|
|
RimuHosting node driver
|
|
"""
|
|
|
|
type = Provider.RIMUHOSTING
|
|
name = 'RimuHosting'
|
|
website = 'http://rimuhosting.com/'
|
|
connectionCls = RimuHostingConnection
|
|
features = {'create_node': ['password']}
|
|
|
|
def __init__(self, key, host=API_HOST, port=443,
|
|
api_context=API_CONTEXT, secure=True):
|
|
"""
|
|
:param key: API key (required)
|
|
:type key: ``str``
|
|
|
|
:param host: hostname for connection
|
|
:type host: ``str``
|
|
|
|
:param port: Override port used for connections.
|
|
:type port: ``int``
|
|
|
|
:param api_context: Optional API context.
|
|
:type api_context: ``str``
|
|
|
|
:param secure: Whether to use HTTPS or HTTP.
|
|
:type secure: ``bool``
|
|
|
|
:rtype: ``None``
|
|
"""
|
|
# Pass in some extra vars so that
|
|
self.key = key
|
|
self.secure = secure
|
|
self.connection = self.connectionCls(key, secure)
|
|
self.connection.host = host
|
|
self.connection.api_context = api_context
|
|
self.connection.port = port
|
|
self.connection.driver = self
|
|
self.connection.connect()
|
|
|
|
def _order_uri(self, node, resource):
|
|
# Returns the order uri with its resourse appended.
|
|
return "/orders/%s/%s" % (node.id, resource)
|
|
|
|
# TODO: Get the node state.
|
|
def _to_node(self, order):
|
|
n = Node(id=order['slug'],
|
|
name=order['domain_name'],
|
|
state=NodeState.RUNNING,
|
|
public_ips=(
|
|
[order['allocated_ips']['primary_ip']] +
|
|
order['allocated_ips']['secondary_ips']),
|
|
private_ips=[],
|
|
driver=self.connection.driver,
|
|
extra={
|
|
'order_oid': order['order_oid'],
|
|
'monthly_recurring_fee': order.get(
|
|
'billing_info').get('monthly_recurring_fee')})
|
|
return n
|
|
|
|
def _to_size(self, plan):
|
|
return NodeSize(
|
|
id=plan['pricing_plan_code'],
|
|
name=plan['pricing_plan_description'],
|
|
ram=plan['minimum_memory_mb'],
|
|
disk=plan['minimum_disk_gb'],
|
|
bandwidth=plan['minimum_data_transfer_allowance_gb'],
|
|
price=plan['monthly_recurring_amt']['amt_usd'],
|
|
driver=self.connection.driver
|
|
)
|
|
|
|
def _to_image(self, image):
|
|
return NodeImage(id=image['distro_code'],
|
|
name=image['distro_description'],
|
|
driver=self.connection.driver)
|
|
|
|
def list_sizes(self, location=None):
|
|
# Returns a list of sizes (aka plans)
|
|
# Get plans. Note this is really just for libcloud.
|
|
# We are happy with any size.
|
|
if location is None:
|
|
location = ''
|
|
else:
|
|
location = ";dc_location=%s" % (location.id)
|
|
|
|
res = self.connection.request(
|
|
'/pricing-plans;server-type=VPS%s' % (location)).object
|
|
return list(map(lambda x: self._to_size(x), res['pricing_plan_infos']))
|
|
|
|
def list_nodes(self):
|
|
# Returns a list of Nodes
|
|
# Will only include active ones.
|
|
res = self.connection.request('/orders;include_inactive=N').object
|
|
return list(map(lambda x: self._to_node(x), res['about_orders']))
|
|
|
|
def list_images(self, location=None):
|
|
# Get all base images.
|
|
# TODO: add other image sources. (Such as a backup of a VPS)
|
|
# All Images are available for use at all locations
|
|
res = self.connection.request('/distributions').object
|
|
return list(map(lambda x: self._to_image(x), res['distro_infos']))
|
|
|
|
def reboot_node(self, node):
|
|
# Reboot
|
|
# PUT the state of RESTARTING to restart a VPS.
|
|
# All data is encoded as JSON
|
|
data = {'reboot_request': {'running_state': 'RESTARTING'}}
|
|
uri = self._order_uri(node, 'vps/running-state')
|
|
self.connection.request(uri, data=json.dumps(data), method='PUT')
|
|
# XXX check that the response was actually successful
|
|
return True
|
|
|
|
def destroy_node(self, node):
|
|
# Shutdown a VPS.
|
|
uri = self._order_uri(node, 'vps')
|
|
self.connection.request(uri, method='DELETE')
|
|
# XXX check that the response was actually successful
|
|
return True
|
|
|
|
def create_node(self, name, size, image, auth=None, ex_billing_oid=None,
|
|
ex_host_server_oid=None, ex_vps_order_oid_to_clone=None,
|
|
ex_num_ips=1, ex_extra_ip_reason=None, ex_memory_mb=None,
|
|
ex_disk_space_mb=None, ex_disk_space_2_mb=None,
|
|
ex_control_panel=None):
|
|
"""Creates a RimuHosting instance
|
|
|
|
@inherits: :class:`NodeDriver.create_node`
|
|
|
|
:keyword name: Must be a FQDN. e.g example.com.
|
|
:type name: ``str``
|
|
|
|
:keyword ex_billing_oid: If not set,
|
|
a billing method is automatically picked.
|
|
:type ex_billing_oid: ``str``
|
|
|
|
:keyword ex_host_server_oid: The host server to set the VPS up on.
|
|
:type ex_host_server_oid: ``str``
|
|
|
|
:keyword ex_vps_order_oid_to_clone: Clone another VPS to use as
|
|
the image for the new VPS.
|
|
:type ex_vps_order_oid_to_clone: ``str``
|
|
|
|
:keyword ex_num_ips: Number of IPs to allocate. Defaults to 1.
|
|
:type ex_num_ips: ``int``
|
|
|
|
:keyword ex_extra_ip_reason: Reason for needing the extra IPs.
|
|
:type ex_extra_ip_reason: ``str``
|
|
|
|
:keyword ex_memory_mb: Memory to allocate to the VPS.
|
|
:type ex_memory_mb: ``int``
|
|
|
|
:keyword ex_disk_space_mb: Diskspace to allocate to the VPS.
|
|
Defaults to 4096 (4GB).
|
|
:type ex_disk_space_mb: ``int``
|
|
|
|
:keyword ex_disk_space_2_mb: Secondary disk size allocation.
|
|
Disabled by default.
|
|
:type ex_disk_space_2_mb: ``int``
|
|
|
|
:keyword ex_control_panel: Control panel to install on the VPS.
|
|
:type ex_control_panel: ``str``
|
|
"""
|
|
# Note we don't do much error checking in this because we
|
|
# expect the API to error out if there is a problem.
|
|
data = {
|
|
'instantiation_options': {
|
|
'domain_name': name,
|
|
'distro': image.id
|
|
},
|
|
'pricing_plan_code': size.id,
|
|
'vps_parameters': {}
|
|
}
|
|
|
|
if ex_control_panel:
|
|
data['instantiation_options']['control_panel'] = \
|
|
ex_control_panel
|
|
|
|
auth = self._get_and_check_auth(auth)
|
|
data['instantiation_options']['password'] = auth.password
|
|
|
|
if ex_billing_oid:
|
|
# TODO check for valid oid.
|
|
data['billing_oid'] = ex_billing_oid
|
|
|
|
if ex_host_server_oid:
|
|
data['host_server_oid'] = ex_host_server_oid
|
|
|
|
if ex_vps_order_oid_to_clone:
|
|
data['vps_order_oid_to_clone'] = ex_vps_order_oid_to_clone
|
|
|
|
if ex_num_ips and int(ex_num_ips) > 1:
|
|
if not ex_extra_ip_reason:
|
|
raise RimuHostingException(
|
|
'Need an reason for having an extra IP')
|
|
else:
|
|
if 'ip_request' not in data:
|
|
data['ip_request'] = {}
|
|
data['ip_request']['num_ips'] = int('ex_num_ips')
|
|
data['ip_request']['extra_ip_reason'] = ex_extra_ip_reason
|
|
|
|
if ex_memory_mb:
|
|
data['vps_parameters']['memory_mb'] = ex_memory_mb
|
|
|
|
if ex_disk_space_mb:
|
|
data['vps_parameters']['disk_space_mb'] = ex_disk_space_mb
|
|
|
|
if ex_disk_space_2_mb:
|
|
data['vps_parameters']['disk_space_2_mb'] = ex_disk_space_2_mb
|
|
|
|
# Don't send empty 'vps_parameters' attribute
|
|
if not data['vps_parameters']:
|
|
del data['vps_parameters']
|
|
|
|
res = self.connection.request(
|
|
'/orders/new-vps',
|
|
method='POST',
|
|
data=json.dumps({"new-vps": data})
|
|
).object
|
|
node = self._to_node(res['about_order'])
|
|
node.extra['password'] = \
|
|
res['new_order_request']['instantiation_options']['password']
|
|
return node
|
|
|
|
def list_locations(self):
|
|
return [
|
|
NodeLocation('DCAUCKLAND', "RimuHosting Auckland", 'NZ', self),
|
|
NodeLocation('DCDALLAS', "RimuHosting Dallas", 'US', self),
|
|
NodeLocation('DCLONDON', "RimuHosting London", 'GB', self),
|
|
NodeLocation('DCSYDNEY', "RimuHosting Sydney", 'AU', self),
|
|
]
|