Source code for libcloud.compute.drivers.rimuhosting

# 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 JsonResponse, ConnectionKey
from libcloud.common.types import InvalidCredsError
from libcloud.compute.base import Node, NodeSize, NodeImage, NodeDriver, NodeLocation
from libcloud.compute.types import Provider, NodeState

API_CONTEXT = "/r"
API_HOST = "rimuhosting.com"


[docs]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])
[docs]class RimuHostingResponse(JsonResponse): """ Response Class for RimuHosting driver """
[docs] def success(self): if self.status == 403: raise InvalidCredsError() return True
[docs] def parse_body(self): try: js = super().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))
[docs]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 )
[docs] 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
[docs] 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)
[docs]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 resource appended. return "/orders/{}/{}".format(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, )
[docs] 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"]))
[docs] 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"]))
[docs] 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"]))
[docs] 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
[docs] 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
[docs] 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
[docs] 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), ]