| # 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. |
| # libcloud.org 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. |
| # Copyright 2009 RedRata Ltd |
| """ |
| RimuHosting Driver |
| """ |
| from libcloud.types import Provider, NodeState, InvalidCredsException |
| from libcloud.base import ConnectionKey, Response, NodeAuthPassword |
| from libcloud.base import NodeDriver, NodeSize, Node, NodeLocation |
| from libcloud.base import NodeImage |
| |
| # JSON is included in the standard library starting with Python 2.6. For 2.5 |
| # and 2.4, there's a simplejson egg at: http://pypi.python.org/pypi/simplejson |
| try: import json |
| except: import simplejson as json |
| |
| # Defaults |
| API_CONTEXT = '/r' |
| API_HOST = 'api.rimuhosting.com' |
| API_PORT = (80,443) |
| API_SECURE = True |
| |
| class RimuHostingException(Exception): |
| def __str__(self): |
| return self.args[0] |
| |
| def __repr__(self): |
| return "<RimuHostingException '%s'>" % (self.args[0]) |
| |
| class RimuHostingResponse(Response): |
| def __init__(self, response): |
| self.body = response.read() |
| self.status = response.status |
| self.headers = dict(response.getheaders()) |
| self.error = response.reason |
| |
| if self.success(): |
| self.object = self.parse_body() |
| |
| def success(self): |
| if self.status == 403: |
| raise InvalidCredsException() |
| return True |
| def parse_body(self): |
| try: |
| js = json.loads(self.body) |
| if js[js.keys()[0]]['response_type'] == "ERROR": |
| raise RimuHostingException( |
| js[js.keys()[0]]['human_readable_message'] |
| ) |
| return js[js.keys()[0]] |
| except ValueError: |
| raise RimuHostingException('Could not parse body: %s' |
| % (self.body)) |
| except KeyError: |
| raise RimuHostingException('Could not parse body: %s' |
| % (self.body)) |
| |
| class RimuHostingConnection(ConnectionKey): |
| |
| api_context = API_CONTEXT |
| host = API_HOST |
| port = API_PORT |
| responseCls = RimuHostingResponse |
| |
| def __init__(self, key, secure=True): |
| # override __init__ so that we can set secure of False for testing |
| ConnectionKey.__init__(self,key,secure) |
| |
| 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={}, data='', headers={}, method='GET'): |
| # Override this method to prepend the api_context |
| return ConnectionKey.request(self, self.api_context + action, |
| params, data, headers, method) |
| |
| class RimuHostingNodeDriver(NodeDriver): |
| type = Provider.RIMUHOSTING |
| name = 'RimuHosting' |
| connectionCls = RimuHostingConnection |
| |
| def __init__(self, key, host=API_HOST, port=API_PORT, |
| api_context=API_CONTEXT, secure=API_SECURE): |
| # 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_ip=( |
| [order['allocated_ips']['primary_ip']] |
| + order['allocated_ips']['secondary_ips'] |
| ), |
| private_ip=[], |
| driver=self.connection.driver, |
| extra={'order_oid': order['order_oid']}) |
| 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 == None: |
| location = ''; |
| else: |
| location = ";dc_location=%s" % (location.id) |
| |
| res = self.connection.request('/pricing-plans;server-type=VPS%s' % (location)).object |
| return 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 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 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, **kwargs): |
| # Creates a RimuHosting instance |
| # |
| # name Must be a FQDN. e.g example.com. |
| # image NodeImage from list_images |
| # size NodeSize from list_sizes |
| # |
| # Keyword arguements supported: |
| # |
| # billing_oid If not set, a billing method is automatically |
| # picked. |
| # |
| # host_server_oid The host server to set the VPS up on. |
| # vps_order_oid_to_clone Clone another VPS to use as the image |
| # for the new VPS. |
| # |
| # num_ips = 1 Number of IPs to allocate. Defaults to 1. |
| # extra_ip_reason Reason for needing the extra IPS. |
| # |
| # memory_mb Memory to allocate to the VPS. |
| # disk_space_mb Diskspace to allocate to the VPS. |
| # Defaults to 4096 (4GB). |
| # disk_space_2_mb Secondary disk size allocation. |
| # Disabled by default. |
| # |
| # pricing_plan_code Plan from list_sizes |
| # |
| # control_panel Control panel to install on the VPS. |
| # |
| # |
| # Note we don't do much error checking in this because we |
| # expect the API to error out if there is a problem. |
| name = kwargs['name'] |
| image = kwargs['image'] |
| size = kwargs['size'] |
| |
| data = { |
| 'instantiation_options':{ |
| 'domain_name': name, 'distro': image.id |
| }, |
| 'pricing_plan_code': size.id, |
| } |
| |
| if kwargs.has_key('control_panel'): |
| data['instantiation_options']['control_panel'] = kwargs['control_panel'] |
| |
| if kwargs.has_key('auth'): |
| auth = kwargs['auth'] |
| if not isinstance(auth, NodeAuthPassword): |
| raise ValueError('auth must be of NodeAuthPassword type') |
| data['instantiation_options']['password'] = auth.password |
| |
| if kwargs.has_key('billing_oid'): |
| #TODO check for valid oid. |
| data['billing_oid'] = kwargs['billing_oid'] |
| |
| if kwargs.has_key('host_server_oid'): |
| data['host_server_oid'] = kwargs['host_server_oid'] |
| |
| if kwargs.has_key('vps_order_oid_to_clone'): |
| data['vps_order_oid_to_clone'] = kwargs['vps_order_oid_to_clone'] |
| |
| if kwargs.has_key('num_ips') and int(kwargs['num_ips']) > 1: |
| if not kwargs.has_key('extra_ip_reason'): |
| raise RimuHostingException('Need an reason for having an extra IP') |
| else: |
| if not data.has_key('ip_request'): |
| data['ip_request'] = {} |
| data['ip_request']['num_ips'] = int(kwargs['num_ips']) |
| data['ip_request']['extra_ip_reason'] = kwargs['extra_ip_reason'] |
| |
| if kwargs.has_key('memory_mb'): |
| if not data.has_key('vps_parameters'): |
| data['vps_parameters'] = {} |
| data['vps_parameters']['memory_mb'] = kwargs['memory_mb'] |
| |
| if kwargs.has_key('disk_space_mb'): |
| if not data.has_key('vps_parameters'): |
| data['vps_parameters'] = {} |
| data['vps_parameters']['disk_space_mb'] = kwargs['disk_space_mb'] |
| |
| if kwargs.has_key('disk_space_2_mb'): |
| if not data.has_key('vps_parameters'): |
| data['vps_parameters'] = {} |
| data['vps_parameters']['disk_space_2_mb'] = kwargs['disk_space_2_mb'] |
| |
| 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), |
| ] |
| |
| features = {"create_node": ["password"]} |
| |