diff --git libcloud/common/cloudstack.py libcloud/common/cloudstack.py new file mode 100644 index 0000000..6db52e2 --- /dev/null +++ libcloud/common/cloudstack.py @@ -0,0 +1,124 @@ +# 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 base64 +import hashlib +import hmac +import time +import urllib + +try: + import json +except: + import simplejson as json + +from libcloud.common.base import ConnectionUserAndKey, Response +from libcloud.common.types import MalformedResponseError + +class CloudStackResponse(Response): + def parse_body(self): + try: + body = json.loads(self.body) + except: + raise MalformedResponseError( + "Failed to parse JSON", + body=self.body, + driver=self.connection.driver) + return body + + parse_error = parse_body + +class CloudStackConnection(ConnectionUserAndKey): + responseCls = CloudStackResponse + + ASYNC_PENDING = 0 + ASYNC_SUCCESS = 1 + ASYNC_FAILURE = 2 + + def _make_signature(self, params): + signature = [(k.lower(), v) for k, v in params.items()] + signature.sort(key=lambda x: x[0]) + signature = urllib.urlencode(signature) + signature = signature.lower().replace('+', '%20') + signature = hmac.new(self.key, msg=signature, digestmod=hashlib.sha1) + return base64.b64encode(signature.digest()) + + def add_default_params(self, params): + params['apiKey'] = self.user_id + params['response'] = 'json' + + return params + + def pre_connect_hook(self, params, headers): + params['signature'] = self._make_signature(params) + + return params, headers + + def _sync_request(self, command, **kwargs): + """This method handles synchronous calls which are generally fast + information retrieval requests and thus return 'quickly'.""" + + kwargs['command'] = command + result = self.request(self.driver.path, params=kwargs) + command = command.lower() + 'response' + if command not in result.object: + raise MalformedResponseError( + "Unknown response format", + body=result.body, + driver=self.driver) + result = result.object[command] + return result + + def _async_request(self, command, **kwargs): + """This method handles asynchronous calls which are generally + requests for the system to do something and can thus take time. + + In these cases the initial call will either fail fast and return + an error, or it can return a job ID. We then poll for the status + of the job ID which can either be pending, successful or failed.""" + + result = self._sync_request(command, **kwargs) + job_id = result['jobid'] + success = True + + while True: + result = self._sync_request('queryAsyncJobResult', jobid=job_id) + status = result.get('jobstatus', self.ASYNC_PENDING) + if status != self.ASYNC_PENDING: + break + time.sleep(self.driver.async_poll_frequency) + + if result['jobstatus'] == self.ASYNC_FAILURE: + raise Exception(result) + + return result['jobresult'] + +class CloudStackDriverMixIn(object): + host = None + path = None + async_poll_frequency = 1 + + connectionCls = CloudStackConnection + + def __init__(self, key, secret=None, secure=True, host=None, port=None): + host = host or self.host + super(CloudStackDriverMixIn, self).__init__(key, secret, secure, host, + port) + + def _sync_request(self, command, **kwargs): + return self.connection._sync_request(command, **kwargs) + + def _async_request(self, command, **kwargs): + return self.connection._async_request(command, **kwargs) diff --git libcloud/compute/drivers/cloudstack.py libcloud/compute/drivers/cloudstack.py new file mode 100644 index 0000000..73eaf2e --- /dev/null +++ libcloud/compute/drivers/cloudstack.py @@ -0,0 +1,267 @@ +# 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 libcloud.common.cloudstack import CloudStackConnection, \ + CloudStackDriverMixIn +from libcloud.compute.base import Node, NodeDriver, NodeImage, NodeLocation, \ + NodeSize +from libcloud.compute.types import DeploymentError, NodeState + +class CloudStackNode(Node): + "Subclass of Node so we can expose our extension methods." + + def ex_allocate_public_ip(self): + "Allocate a public IP and bind it to this node." + return self.driver.ex_allocate_public_ip(self) + + def ex_release_public_ip(self, address): + "Release a public IP that this node holds." + return self.driver.ex_release_public_ip(self, address) + + def ex_add_ip_forwarding_rule(self, address, protocol, start_port, + end_port=None): + "Add a NAT/firewall forwarding rule for a port or ports." + return self.driver.ex_add_ip_forwarding_rule(self, address, protocol, + start_port, end_port) + + def ex_delete_ip_forwarding_rule(self, rule): + "Delete a NAT/firewall rule." + return self.driver.ex_delete_ip_forwarding_rule(self, rule) + +class CloudStackAddress(object): + "A public IP address." + + def __init__(self, node, id, address): + self.node = node + self.id = id + self.address = address + + def release(self): + self.node.ex_release_public_ip(self) + + def __str__(self): + return self.address + + def __eq__(self, other): + return self.__class__ is other.__class__ and self.id == other.id + +class CloudStackForwardingRule(object): + "A NAT/firewall forwarding rule." + + def __init__(self, node, id, address, protocol, start_port, end_port=None): + self.node = node + self.id = id + self.address = address + self.protocol = protocol + self.start_port = start_port + self.end_port = end_port + + def delete(self): + self.node.ex_delete_ip_forwarding_rule(self) + + def __eq__(self, other): + return self.__class__ is other.__class__ and self.id == other.id + +class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): + """Driver for the CloudStack API. + + @cvar host: The host where the API can be reached. + @cvar path: The path where the API can be reached. + @cvar async_poll_frequency: How often (in seconds) to poll for async + job completion. + @type async_poll_frequency: C{int}""" + + api_name = 'cloudstack' + + NODE_STATE_MAP = { + 'Running': NodeState.RUNNING, + 'Starting': NodeState.REBOOTING, + 'Stopped': NodeState.TERMINATED, + 'Stopping': NodeState.TERMINATED + } + + def list_images(self, location=None): + args = { + 'templatefilter': 'executable' + } + if location is not None: + args['zoneid'] = location.id + imgs = self._sync_request('listTemplates', **args) + images = [] + for img in imgs['template']: + images.append(NodeImage(img['id'], img['name'], self, { + 'hypervisor': img['hypervisor'], + 'format': img['format'], + 'os': img['ostypename'], + })) + return images + + def list_locations(self): + locs = self._sync_request('listZones') + locations = [] + for loc in locs['zone']: + locations.append(NodeLocation(loc['id'], loc['name'], 'AU', self)) + return locations + + def list_nodes(self): + vms = self._sync_request('listVirtualMachines') + addrs = self._sync_request('listPublicIpAddresses') + + public_ips = {} + for addr in addrs['publicipaddress']: + if 'virtualmachineid' not in addr: + continue + vm_id = addr['virtualmachineid'] + if vm_id not in public_ips: + public_ips[vm_id] = {} + public_ips[vm_id][addr['ipaddress']] = addr['id'] + + nodes = [] + + for vm in vms.get('virtualmachine', []): + node = CloudStackNode( + id=vm['id'], + name=vm.get('displayname', None), + state=self.NODE_STATE_MAP[vm['state']], + public_ip=public_ips.get(vm['id'], {}).keys(), + private_ip=[x['ipaddress'] for x in vm['nic']], + driver=self, + extra={ + 'zoneid': vm['zoneid'], + } + ) + + addrs = public_ips.get(vm['id'], {}).items() + addrs = [CloudStackAddress(node, v, k) for k, v in addrs] + node.extra['ip_addresses'] = addrs + + rules = [] + for addr in addrs: + result = self._sync_request('listIpForwardingRules') + for r in result.get('ipforwardingrule', []): + rule = CloudStackForwardingRule(node, r['id'], addr, + r['protocol'].upper(), + r['startport'], + r['endport']) + rules.append(rule) + node.extra['ip_forwarding_rules'] = rules + + nodes.append(node) + + return nodes + + def list_sizes(self, location=None): + szs = self._sync_request('listServiceOfferings') + sizes = [] + for sz in szs['serviceoffering']: + sizes.append(NodeSize(sz['id'], sz['name'], sz['memory'], 0, 0, + 0, self)) + return sizes + + def create_node(self, name, size, image, location=None, **kwargs): + if location is None: + location = self.list_locations()[0] + + networks = self._sync_request('listNetworks') + network_id = networks['network'][0]['id'] + + result = self._async_request('deployVirtualMachine', + name=name, + displayname=name, + serviceofferingid=size.id, + templateid=image.id, + zoneid=location.id, + networkids=network_id, + ) + + node = result['virtualmachine'] + + return Node( + id=node['id'], + name=node['displayname'], + state=self.NODE_STATE_MAP[node['state']], + public_ip=[], + private_ip=[x['ipaddress'] for x in node['nic']], + driver=self, + extra={ + 'zoneid': location.id, + 'ip_addresses': [], + 'forwarding_rules': [], + } + ) + + def destroy_node(self, node): + self._async_request('destroyVirtualMachine', id=node.id) + return True + + def reboot_node(self, node): + self._async_request('rebootVirtualMachine', id=node.id) + return True + + def ex_allocate_public_ip(self, node): + "Allocate a public IP and bind it to a node." + + zoneid = node.extra['zoneid'] + addr = self._async_request('associateIpAddress', zoneid=zoneid) + addr = addr['ipaddress'] + result = self._sync_request('enableStaticNat', virtualmachineid=node.id, + ipaddressid=addr['id']) + if result.get('success', '').lower() != 'true': + return None + + node.public_ip.append(addr['ipaddress']) + addr = CloudStackAddress(node, addr['id'], addr['ipaddress']) + node.extra['ip_addresses'].append(addr) + return addr + + def ex_release_public_ip(self, node, address): + "Release a public IP." + + node.extra['ip_addresses'].remove(address) + node.public_ip.remove(address.address) + + self._async_request('disableStaticNat', ipaddressid=address.id) + self._async_request('disassociateIpAddress', id=address.id) + return True + + def ex_add_ip_forwarding_rule(self, node, address, protocol, + start_port, end_port=None): + "Add a NAT/firewall forwarding rule." + + protocol = protocol.upper() + if protocol not in ('TCP', 'UDP'): + return None + + args = { + 'ipaddressid': address.id, + 'protocol': protocol, + 'startport': int(start_port) + } + if end_port is not None: + args['endport'] = int(end_port) + + result = self._async_request('createIpForwardingRule', **args) + result = result['ipforwardingrule'] + rule = CloudStackForwardingRule(node, result['id'], address, + protocol, start_port, end_port) + node.extra['ip_forwarding_rules'].append(rule) + return rule + + def ex_delete_ip_forwarding_rule(self, node, rule): + "Remove a NAT/firewall forwading rule." + + node.extra['ip_forwarding_rules'].remove(rule) + self._async_request('deleteIpForwardingRule', id=rule.id) + return True diff --git libcloud/compute/drivers/ninefold.py libcloud/compute/drivers/ninefold.py new file mode 100644 index 0000000..945c72e --- /dev/null +++ libcloud/compute/drivers/ninefold.py @@ -0,0 +1,27 @@ +# 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 libcloud.compute.providers import Provider + +from libcloud.compute.drivers.cloudstack import CloudStackNodeDriver + +class NinefoldNodeDriver(CloudStackNodeDriver): + "Driver for Ninefold's Compute platform." + + host = 'api.ninefold.com' + path = '/compute/v1.0/' + + type = Provider.NINEFOLD + name = 'Ninefold' diff --git libcloud/compute/providers.py libcloud/compute/providers.py index 3b5c23c..ffaa549 100644 --- libcloud/compute/providers.py +++ libcloud/compute/providers.py @@ -89,6 +89,8 @@ DRIVERS = { ('libcloud.compute.drivers.opsource', 'OpsourceNodeDriver'), Provider.OPENSTACK: ('libcloud.compute.drivers.rackspace', 'OpenStackNodeDriver'), + Provider.NINEFOLD: + ('libcloud.compute.drivers.ninefold', 'NinefoldNodeDriver'), } def get_driver(provider): diff --git libcloud/compute/types.py libcloud/compute/types.py index 32056aa..fd1af9e 100644 --- libcloud/compute/types.py +++ libcloud/compute/types.py @@ -54,6 +54,7 @@ class Provider(object): @cvar NIMBUS: Nimbus @cvar BLUEBOX: Bluebox @cvar OPSOURCE: Opsource Cloud + @cvar NINEFOLD: Ninefold """ DUMMY = 0 EC2 = 1 # deprecated name @@ -91,6 +92,7 @@ class Provider(object): OPENSTACK = 31 SKALICLOUD = 32 SERVERLOVE = 33 + NINEFOLD = 34 class NodeState(object): """ diff --git libcloud/loadbalancer/base.py libcloud/loadbalancer/base.py index 45b6f16..7254954 100644 --- libcloud/loadbalancer/base.py +++ libcloud/loadbalancer/base.py @@ -83,7 +83,7 @@ class Driver(object): _ALGORITHM_TO_VALUE_MAP = {} _VALUE_TO_ALGORITHM_MAP = {} - def __init__(self, key, secret=None, secure=True): + def __init__(self, key, secret=None, secure=True, host=None, port=None): self.key = key self.secret = secret args = [self.key] @@ -93,6 +93,12 @@ class Driver(object): args.append(secure) + if host != None: + args.append(host) + + if port != None: + args.append(port) + self.connection = self.connectionCls(*args) self.connection.driver = self self.connection.connect() diff --git libcloud/loadbalancer/drivers/cloudstack.py libcloud/loadbalancer/drivers/cloudstack.py new file mode 100644 index 0000000..12fb100 --- /dev/null +++ libcloud/loadbalancer/drivers/cloudstack.py @@ -0,0 +1,123 @@ +# 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 libcloud.common.cloudstack import CloudStackConnection, \ + CloudStackDriverMixIn +from libcloud.loadbalancer.base import LoadBalancer, Member, Driver, Algorithm +from libcloud.loadbalancer.base import DEFAULT_ALGORITHM +from libcloud.loadbalancer.types import State, LibcloudLBImmutableError +from libcloud.utils import reverse_dict + +class CloudStackLBDriver(CloudStackDriverMixIn, Driver): + """Driver for CloudStack load balancers.""" + + api_name = 'cloudstack_lb' + + _VALUE_TO_ALGORITHM_MAP = { + 'roundrobin': Algorithm.ROUND_ROBIN, + 'leastconn': Algorithm.LEAST_CONNECTIONS + } + _ALGORITHM_TO_VALUE_MAP = reverse_dict(_VALUE_TO_ALGORITHM_MAP) + + LB_STATE_MAP = { + 'Active': State.RUNNING, + } + + def list_protocols(self): + """We don't actually have any protocol awareness beyond TCP.""" + return [ 'tcp' ] + + def list_balancers(self): + balancers = self._sync_request('listLoadBalancerRules') + balancers = balancers.get('loadbalancerrule', []) + return [self._to_balancer(balancer) for balancer in balancers] + + def get_balancer(self, balancer_id): + balancer = self._sync_request('listLoadBalancerRules', id=balancer_id) + balancer = balancer.get('loadbalancerrule', []) + if not balancer: + raise Exception("no such load balancer: " + str(balancer_id)) + return self._to_balancer(balancer[0]) + + def create_balancer(self, name, members, protocol='http', port=80, + algorithm=DEFAULT_ALGORITHM, location=None, + private_port=None): + if location is None: + locations = self._sync_request('listZones') + location = locations['zone'][0]['id'] + else: + location = location.id + if private_port is None: + private_port = port + + result = self._async_request('associateIpAddress', zoneid=location) + public_ip = result['ipaddress'] + + result = self._sync_request('createLoadBalancerRule', + algorithm=self._ALGORITHM_TO_VALUE_MAP[algorithm], + name=name, + privateport=private_port, + publicport=port, + publicipid=public_ip['id'], + ) + + balancer = self._to_balancer(result['loadbalancer']) + + for member in members: + balancer.attach_member(member) + + return balancer + + def destroy_balancer(self, balancer): + self._async_request('deleteLoadBalancerRule', id=balancer.id) + self._async_request('disassociateIpAddress', + id=balancer.ex_public_ip_id) + + def balancer_attach_member(self, balancer, member): + member.port = balancer.ex_private_port + self._async_request('assignToLoadBalancerRule', id=balancer.id, + virtualmachineids=member.id) + return True + + def balancer_detach_member(self, balancer, member): + self._async_request('removeFromLoadBalancerRule', id=balancer.id, + virtualmachineids=member.id) + return True + + def balancer_list_members(self, balancer): + members = self._sync_request('listLoadBalancerRuleInstances', + id=balancer.id) + members = members['loadbalancerruleinstance'] + return [self._to_member(m, balancer.ex_private_port) for m in members] + + def _to_balancer(self, obj): + balancer = LoadBalancer( + id=obj['id'], + name=obj['name'], + state=self.LB_STATE_MAP.get(obj['state'], State.UNKNOWN), + ip=obj['publicip'], + port=obj['publicport'], + driver=self.connection.driver + ) + balancer.ex_private_port = obj['privateport'] + balancer.ex_public_ip_id = obj['publicipid'] + return balancer + + def _to_member(self, obj, port): + return Member( + id=obj['id'], + ip=obj['nic'][0]['ipaddress'], + port=port + ) diff --git libcloud/loadbalancer/drivers/ninefold.py libcloud/loadbalancer/drivers/ninefold.py new file mode 100644 index 0000000..d9d434e --- /dev/null +++ libcloud/loadbalancer/drivers/ninefold.py @@ -0,0 +1,27 @@ +# 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 libcloud.loadbalancer.providers import Provider + +from libcloud.loadbalancer.drivers.cloudstack import CloudStackLBDriver + +class NinefoldLBDriver(CloudStackLBDriver): + "Driver for load balancers on Ninefold's Compute platform." + + host = 'api.ninefold.com' + path = '/compute/v1.0/' + + type = Provider.NINEFOLD + name = 'Ninefold LB' diff --git libcloud/loadbalancer/providers.py libcloud/loadbalancer/providers.py index fb12e82..a727b57 100644 --- libcloud/loadbalancer/providers.py +++ libcloud/loadbalancer/providers.py @@ -27,6 +27,8 @@ DRIVERS = { ('libcloud.loadbalancer.drivers.rackspace', 'RackspaceLBDriver'), Provider.GOGRID: ('libcloud.loadbalancer.drivers.gogrid', 'GoGridLBDriver'), + Provider.NINEFOLD: + ('libcloud.loadbalancer.drivers.ninefold', 'NinefoldLBDriver'), } def get_driver(provider): diff --git libcloud/loadbalancer/types.py libcloud/loadbalancer/types.py index 79c2144..967f7e7 100644 --- libcloud/loadbalancer/types.py +++ libcloud/loadbalancer/types.py @@ -29,6 +29,7 @@ class LibcloudLBImmutableError(LibcloudLBError): pass class Provider(object): RACKSPACE_US = 0 GOGRID = 1 + NINEFOLD = 2 class State(object): """ diff --git setup.py setup.py index dcb5d24..183f7b4 100644 --- setup.py +++ setup.py @@ -29,7 +29,8 @@ libcloud.utils.SHOW_DEPRECATION_WARNING = False HTML_VIEWSOURCE_BASE = 'https://svn.apache.org/viewvc/libcloud/trunk' PROJECT_BASE_DIR = 'http://libcloud.apache.org' -TEST_PATHS = [ 'test', 'test/compute', 'test/storage' , 'test/loadbalancer'] +TEST_PATHS = ['test', 'test/common', 'test/compute', 'test/storage', + 'test/loadbalancer'] DOC_TEST_MODULES = [ 'libcloud.compute.drivers.dummy', 'libcloud.storage.drivers.dummy' ] diff --git test/common/__init__.py test/common/__init__.py new file mode 100644 index 0000000..ae1e83e --- /dev/null +++ test/common/__init__.py @@ -0,0 +1,14 @@ +# 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. diff --git test/common/test_cloudstack.py test/common/test_cloudstack.py new file mode 100644 index 0000000..2156b53 --- /dev/null +++ test/common/test_cloudstack.py @@ -0,0 +1,181 @@ +import httplib +import sys +import unittest +import urlparse + +try: + import json +except: + import simplejson as json + +from libcloud.common.cloudstack import CloudStackConnection, CloudStackResponse +from libcloud.common.types import MalformedResponseError + +from test import MockHttpTestCase + +async_delay = 0 + +class CloudStackMockDriver(object): + host = 'nonexistant.' + path = '/path' + async_poll_frequency = 0 + + name = 'fake' + + async_delay = 0 + +class CloudStackCommonTest(unittest.TestCase): + def setUp(self): + CloudStackConnection.conn_classes = (None, CloudStackMockHttp) + self.connection = CloudStackConnection('apikey', 'secret', + host=CloudStackMockDriver.host) + self.driver = self.connection.driver = CloudStackMockDriver() + + def test_sync_request_bad_response(self): + self.driver.path = '/bad/response' + try: + self.connection._sync_request('fake') + except Exception, e: + self.assertTrue(isinstance(e, MalformedResponseError)) + return + self.assertTrue(False) + + def test_sync_request(self): + self.driver.path = '/sync' + self.connection._sync_request('fake') + + def test_async_request_successful(self): + self.driver.path = '/async/success' + result = self.connection._async_request('fake') + self.assertEqual(result, {'fake': 'result'}) + + def test_async_request_unsuccessful(self): + self.driver.path = '/async/fail' + try: + self.connection._async_request('fake') + except: + return + self.assertFalse(True) + + def test_async_request_delayed(self): + global async_delay + self.driver.path = '/async/delayed' + async_delay = 2 + self.connection._async_request('fake') + self.assertEqual(async_delay, 0) + + def test_signature_algorithm(self): + cases = [ + ( + { + 'command': 'listVirtualMachines' + }, 'z/a9Y7J52u48VpqIgiwaGUMCso0=' + ), ( + { + 'command': 'deployVirtualMachine', + 'name': 'fred', + 'displayname': 'George', + 'serviceofferingid': 5, + 'templateid': 17, + 'zoneid': 23, + 'networkids': 42 + }, 'gHTo7mYmadZ+zluKHzlEKb1i/QU=' + ), ( + { + 'command': 'deployVirtualMachine', + 'name': 'fred', + 'displayname': 'George+Ringo', + 'serviceofferingid': 5, + 'templateid': 17, + 'zoneid': 23, + 'networkids': 42 + }, 'tAgfrreI1ZvWlWLClD3gu4+aKv4=' + ) + ] + + connection = CloudStackConnection('fnord', 'abracadabra') + for case in cases: + params = connection.add_default_params(case[0]) + self.assertEqual(connection._make_signature(params), case[1]) + +class CloudStackMockHttp(MockHttpTestCase): + def _response(self, status, result, response): + return (status, json.dumps(result), result, response) + + def _check_request(self, url): + url = urlparse.urlparse(url) + query = dict(urlparse.parse_qsl(url.query)) + + self.assertTrue('apiKey' in query) + self.assertTrue('command' in query) + self.assertTrue('response' in query) + self.assertTrue('signature' in query) + + self.assertTrue(query['response'] == 'json') + + return query + + def _bad_response(self, method, url, body, headers): + self._check_request(url) + result = {'success': True} + return self._response(httplib.OK, result, httplib.responses[httplib.OK]) + + def _sync(self, method, url, body, headers): + query = self._check_request(url) + result = {query['command'].lower() + 'response': {}} + return self._response(httplib.OK, result, httplib.responses[httplib.OK]) + + def _async_success(self, method, url, body, headers): + query = self._check_request(url) + if query['command'].lower() == 'queryasyncjobresult': + self.assertEqual(query['jobid'], '42') + result = { + query['command'].lower() + 'response': { + 'jobstatus': 1, + 'jobresult': {'fake': 'result'} + } + } + else: + result = {query['command'].lower() + 'response': {'jobid': '42'}} + return self._response(httplib.OK, result, httplib.responses[httplib.OK]) + + def _async_fail(self, method, url, body, headers): + query = self._check_request(url) + if query['command'].lower() == 'queryasyncjobresult': + self.assertEqual(query['jobid'], '42') + result = { + query['command'].lower() + 'response': { + 'jobstatus': 2, + 'jobresult': {'fake': 'failresult'} + } + } + else: + result = {query['command'].lower() + 'response': {'jobid': '42'}} + return self._response(httplib.OK, result, httplib.responses[httplib.OK]) + + def _async_delayed(self, method, url, body, headers): + global async_delay + + query = self._check_request(url) + if query['command'].lower() == 'queryasyncjobresult': + self.assertEqual(query['jobid'], '42') + if async_delay == 0: + result = { + query['command'].lower() + 'response': { + 'jobstatus': 1, + 'jobresult': {'fake': 'result'} + } + } + else: + result = { + query['command'].lower() + 'response': { + 'jobstatus': 0, + } + } + async_delay -= 1 + else: + result = {query['command'].lower() + 'response': {'jobid': '42'}} + return self._response(httplib.OK, result, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git test/compute/fixtures/cloudstack/deployVirtualMachine_default.json test/compute/fixtures/cloudstack/deployVirtualMachine_default.json new file mode 100644 index 0000000..5fbec21 --- /dev/null +++ test/compute/fixtures/cloudstack/deployVirtualMachine_default.json @@ -0,0 +1 @@ +{ "deployvirtualmachineresponse" : {"jobid":17164,"id":2602} } diff --git test/compute/fixtures/cloudstack/deployVirtualMachine_deployfail.json test/compute/fixtures/cloudstack/deployVirtualMachine_deployfail.json new file mode 100644 index 0000000..2162fe1 --- /dev/null +++ test/compute/fixtures/cloudstack/deployVirtualMachine_deployfail.json @@ -0,0 +1 @@ +{ "deployvirtualmachineresponse" : {"errorcode" : 431, "errortext" : "Unable to find service offering: 104"} } diff --git test/compute/fixtures/cloudstack/deployVirtualMachine_deployfail2.json test/compute/fixtures/cloudstack/deployVirtualMachine_deployfail2.json new file mode 100644 index 0000000..05790d2 --- /dev/null +++ test/compute/fixtures/cloudstack/deployVirtualMachine_deployfail2.json @@ -0,0 +1 @@ +{ "deployvirtualmachineresponse" : {"jobid":17177,"id":2602} } diff --git test/compute/fixtures/cloudstack/destroyVirtualMachine_default.json test/compute/fixtures/cloudstack/destroyVirtualMachine_default.json new file mode 100644 index 0000000..dfd664f --- /dev/null +++ test/compute/fixtures/cloudstack/destroyVirtualMachine_default.json @@ -0,0 +1 @@ +{ "destroyvirtualmachineresponse" : {"jobid":17166} } diff --git test/compute/fixtures/cloudstack/listNetworks_default.json test/compute/fixtures/cloudstack/listNetworks_default.json new file mode 100644 index 0000000..2701347 --- /dev/null +++ test/compute/fixtures/cloudstack/listNetworks_default.json @@ -0,0 +1 @@ +{ "listnetworksresponse" : { "network" : [ {"id":860,"name":"Virtual Network","displaytext":"A dedicated virtualized network for your account. The broadcast domain is contained within a VLAN and all public network access is routed out by a virtual router.","broadcastdomaintype":"Vlan","traffictype":"Guest","zoneid":1,"networkofferingid":6,"networkofferingname":"DefaultVirtualizedNetworkOffering","networkofferingdisplaytext":"Virtual Vlan","networkofferingavailability":"Required","isshared":false,"issystem":false,"state":"Implemented","related":860,"broadcasturi":"vlan://1459","dns1":"1.1.1.1","dns2":"1.1.1.2","type":"Virtual","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","isdefault":true,"service":[{"name":"Gateway"},{"name":"Firewall","capability":[{"name":"MultipleIps","value":"true"},{"name":"TrafficStatistics","value":"per public ip"},{"name":"StaticNat","value":"true"},{"name":"SupportedProtocols","value":"tcp,udp"},{"name":"SupportedSourceNatTypes","value":"per account"}]},{"name":"UserData"},{"name":"Dns"},{"name":"Dhcp"},{"name":"Lb","capability":[{"name":"TrafficStatistics","value":"per public ip"},{"name":"SupportedProtocols","value":"tcp,udp"},{"name":"SupportedLbAlgorithms","value":"roundrobin,leastconn"}]}],"networkdomain":"cs363local","securitygroupenabled":false} ] } } diff --git test/compute/fixtures/cloudstack/listNetworks_deployfail.json test/compute/fixtures/cloudstack/listNetworks_deployfail.json new file mode 100644 index 0000000..2701347 --- /dev/null +++ test/compute/fixtures/cloudstack/listNetworks_deployfail.json @@ -0,0 +1 @@ +{ "listnetworksresponse" : { "network" : [ {"id":860,"name":"Virtual Network","displaytext":"A dedicated virtualized network for your account. The broadcast domain is contained within a VLAN and all public network access is routed out by a virtual router.","broadcastdomaintype":"Vlan","traffictype":"Guest","zoneid":1,"networkofferingid":6,"networkofferingname":"DefaultVirtualizedNetworkOffering","networkofferingdisplaytext":"Virtual Vlan","networkofferingavailability":"Required","isshared":false,"issystem":false,"state":"Implemented","related":860,"broadcasturi":"vlan://1459","dns1":"1.1.1.1","dns2":"1.1.1.2","type":"Virtual","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","isdefault":true,"service":[{"name":"Gateway"},{"name":"Firewall","capability":[{"name":"MultipleIps","value":"true"},{"name":"TrafficStatistics","value":"per public ip"},{"name":"StaticNat","value":"true"},{"name":"SupportedProtocols","value":"tcp,udp"},{"name":"SupportedSourceNatTypes","value":"per account"}]},{"name":"UserData"},{"name":"Dns"},{"name":"Dhcp"},{"name":"Lb","capability":[{"name":"TrafficStatistics","value":"per public ip"},{"name":"SupportedProtocols","value":"tcp,udp"},{"name":"SupportedLbAlgorithms","value":"roundrobin,leastconn"}]}],"networkdomain":"cs363local","securitygroupenabled":false} ] } } diff --git test/compute/fixtures/cloudstack/listNetworks_deployfail2.json test/compute/fixtures/cloudstack/listNetworks_deployfail2.json new file mode 100644 index 0000000..2701347 --- /dev/null +++ test/compute/fixtures/cloudstack/listNetworks_deployfail2.json @@ -0,0 +1 @@ +{ "listnetworksresponse" : { "network" : [ {"id":860,"name":"Virtual Network","displaytext":"A dedicated virtualized network for your account. The broadcast domain is contained within a VLAN and all public network access is routed out by a virtual router.","broadcastdomaintype":"Vlan","traffictype":"Guest","zoneid":1,"networkofferingid":6,"networkofferingname":"DefaultVirtualizedNetworkOffering","networkofferingdisplaytext":"Virtual Vlan","networkofferingavailability":"Required","isshared":false,"issystem":false,"state":"Implemented","related":860,"broadcasturi":"vlan://1459","dns1":"1.1.1.1","dns2":"1.1.1.2","type":"Virtual","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","isdefault":true,"service":[{"name":"Gateway"},{"name":"Firewall","capability":[{"name":"MultipleIps","value":"true"},{"name":"TrafficStatistics","value":"per public ip"},{"name":"StaticNat","value":"true"},{"name":"SupportedProtocols","value":"tcp,udp"},{"name":"SupportedSourceNatTypes","value":"per account"}]},{"name":"UserData"},{"name":"Dns"},{"name":"Dhcp"},{"name":"Lb","capability":[{"name":"TrafficStatistics","value":"per public ip"},{"name":"SupportedProtocols","value":"tcp,udp"},{"name":"SupportedLbAlgorithms","value":"roundrobin,leastconn"}]}],"networkdomain":"cs363local","securitygroupenabled":false} ] } } diff --git test/compute/fixtures/cloudstack/listPublicIpAddresses_default.json test/compute/fixtures/cloudstack/listPublicIpAddresses_default.json new file mode 100644 index 0000000..b9b8527 --- /dev/null +++ test/compute/fixtures/cloudstack/listPublicIpAddresses_default.json @@ -0,0 +1 @@ +{ "listpublicipaddressesresponse" : { "publicipaddress" : [ {"id":34000,"ipaddress":"1.1.1.49","allocated":"2011-06-23T05:20:39+0000","zoneid":1,"zonename":"Sydney","issourcenat":false,"account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","forvirtualnetwork":true,"isstaticnat":false,"associatednetworkid":860,"networkid":200,"state":"Allocated"}, {"id":33999,"ipaddress":"1.1.1.48","allocated":"2011-06-23T05:20:34+0000","zoneid":1,"zonename":"Sydney","issourcenat":false,"account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","forvirtualnetwork":true,"isstaticnat":false,"associatednetworkid":860,"networkid":200,"state":"Allocated"}, {"id":33998,"ipaddress":"1.1.1.47","allocated":"2011-06-23T05:20:30+0000","zoneid":1,"zonename":"Sydney","issourcenat":false,"account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","forvirtualnetwork":true,"isstaticnat":false,"associatednetworkid":860,"networkid":200,"state":"Allocated"}, {"id":33970,"ipaddress":"1.1.1.19","allocated":"2011-06-20T04:08:34+0000","zoneid":1,"zonename":"Sydney","issourcenat":true,"account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","forvirtualnetwork":true,"isstaticnat":false,"associatednetworkid":860,"networkid":200,"state":"Allocated"} ] } } diff --git test/compute/fixtures/cloudstack/listServiceOfferings_default.json test/compute/fixtures/cloudstack/listServiceOfferings_default.json new file mode 100644 index 0000000..a56d890 --- /dev/null +++ test/compute/fixtures/cloudstack/listServiceOfferings_default.json @@ -0,0 +1 @@ +{ "listserviceofferingsresponse" : { "serviceoffering" : [ {"id":105,"name":"Compute Micro PRD","displaytext":"1CPU, 384MB, 80GB HDD","cpunumber":1,"cpuspeed":1200,"memory":384,"created":"2011-06-01T03:38:05+0000","storagetype":"shared","offerha":false,"domainid":14,"domain":"AA000062"}, {"id":70,"name":"Compute XLarge PRD","displaytext":"8CPU, 13.6GB RAM, 160GB Storage","cpunumber":8,"cpuspeed":1200,"memory":13928,"created":"2011-02-08T07:06:19+0000","storagetype":"shared","offerha":true,"domainid":14,"domain":"AA000062"}, {"id":69,"name":"Compute Large PRD","displaytext":"4CPU, 6.8GB RAM, 160GB Storage","cpunumber":4,"cpuspeed":1200,"memory":6964,"created":"2011-02-08T07:05:47+0000","storagetype":"shared","offerha":true,"domainid":14,"domain":"AA000062"}, {"id":68,"name":"Compute Medium PRD","displaytext":"2CPU, 3.4GB RAM, 160GB Storage","cpunumber":2,"cpuspeed":1200,"memory":3484,"created":"2011-02-08T07:05:03+0000","storagetype":"shared","offerha":true,"domainid":14,"domain":"AA000062"}, {"id":67,"name":"Compute Small PRD","displaytext":"1CPU, 1.7GB RAM, 160GB Storage","cpunumber":1,"cpuspeed":1200,"memory":1744,"created":"2011-02-08T07:03:44+0000","storagetype":"shared","offerha":true,"domainid":14,"domain":"AA000062"} ] } } diff --git test/compute/fixtures/cloudstack/listTemplates_default.json test/compute/fixtures/cloudstack/listTemplates_default.json new file mode 100644 index 0000000..b7f9900 --- /dev/null +++ test/compute/fixtures/cloudstack/listTemplates_default.json @@ -0,0 +1 @@ +{ "listtemplatesresponse" : { "template" : [ {"id":576,"name":"ESX[beta] Ubuntu 10.04.2 CHEF Small \\ Micro Optimised","displaytext":"ESX[beta] Ubuntu 10.04.2 CHEF Small \\ Micro Optimised","ispublic":true,"created":"2011-06-01T01:25:12+0000","isready":true,"passwordenabled":false,"format":"OVA","isfeatured":true,"crossZones":false,"ostypeid":126,"ostypename":"Ubuntu 10.04 (64-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":702743552,"templatetype":"USER","hypervisor":"VMware","domain":"ROOT","domainid":1,"isextractable":false}, {"id":443,"name":"XEN Basic Windows Svr 2008 R2 x64 R2.1","displaytext":"XEN Basic Windows Svr 2008 R2 x64 R2.1","ispublic":true,"created":"2011-03-25T01:29:46+0000","isready":true,"passwordenabled":false,"format":"VHD","isfeatured":true,"crossZones":false,"ostypeid":54,"ostypename":"Windows Server 2008 R2 (64-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":171798691840,"templatetype":"USER","hypervisor":"XenServer","domain":"ROOT","domainid":1,"isextractable":false}, {"id":474,"name":"XEN Basic Windows Svr 2003 SP2 STD","displaytext":"XEN Basic Windows Svr 2003 SP2 STD","ispublic":true,"created":"2011-04-07T10:38:45+0000","isready":true,"passwordenabled":false,"format":"VHD","isfeatured":true,"crossZones":false,"ostypeid":89,"ostypename":"Windows Server 2003 Standard Edition(32-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":171798691840,"templatetype":"USER","hypervisor":"XenServer","domain":"ROOT","domainid":1,"isextractable":false}, {"id":444,"name":"ESX[beta] Windows 2003 x32 R2.0","displaytext":"ESX[beta] Windows 2003 x32 R2.0","ispublic":true,"created":"2011-03-25T01:34:00+0000","isready":true,"passwordenabled":false,"format":"OVA","isfeatured":true,"crossZones":false,"ostypeid":89,"ostypename":"Windows Server 2003 Standard Edition(32-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":876909056,"templatetype":"USER","hypervisor":"VMware","domain":"ROOT","domainid":1,"isextractable":false}, {"id":447,"name":"ESX[beta] Windows 2008 x32 R2.0","displaytext":"ESX[beta] Windows 2008 x32 R2.0","ispublic":true,"created":"2011-03-25T01:45:23+0000","isready":true,"passwordenabled":false,"format":"OVA","isfeatured":true,"crossZones":false,"ostypeid":52,"ostypename":"Windows Server 2008 (32-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":3391547904,"templatetype":"USER","hypervisor":"VMware","domain":"ROOT","domainid":1,"isextractable":false}, {"id":462,"name":"ESX[beta] Centos 5.5 x64 R2.0","displaytext":"ESX[beta] Centos 5.5 x64 R2.0","ispublic":true,"created":"2011-03-28T05:06:36+0000","isready":true,"passwordenabled":false,"format":"OVA","isfeatured":true,"crossZones":false,"ostypeid":12,"ostypename":"CentOS 5.3 (64-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":2263178240,"templatetype":"USER","hypervisor":"VMware","domain":"ROOT","domainid":1,"isextractable":false}, {"id":425,"name":"XEN Windows 2008 x32 R2.0","displaytext":"XEN Windows 2008 x32 R2.0","ispublic":true,"created":"2011-03-22T03:22:21+0000","isready":true,"passwordenabled":false,"format":"VHD","isfeatured":true,"crossZones":false,"ostypeid":52,"ostypename":"Windows Server 2008 (32-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":171798691840,"templatetype":"USER","hypervisor":"XenServer","domain":"ROOT","domainid":1,"isextractable":false}, {"id":461,"name":"ESX[beta] Basic Windows 2008 R2 x64","displaytext":"ESX[beta] Basic Windows 2008 R2 x64","ispublic":true,"created":"2011-03-26T22:48:48+0000","isready":true,"passwordenabled":false,"format":"OVA","isfeatured":true,"crossZones":false,"ostypeid":54,"ostypename":"Windows Server 2008 R2 (64-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":3230146048,"templatetype":"USER","hypervisor":"VMware","domain":"ROOT","domainid":1,"isextractable":false}, {"id":575,"name":"Xen Ubuntu 10.04.2 CHEF Small \\ Micro Optimised","displaytext":"Xen Ubuntu 10.04.2 CHEF Small \\ Micro Optimised","ispublic":true,"created":"2011-06-01T01:06:21+0000","isready":true,"passwordenabled":false,"format":"VHD","isfeatured":true,"crossZones":false,"ostypeid":12,"ostypename":"CentOS 5.3 (64-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":85899345920,"templatetype":"USER","hypervisor":"XenServer","domain":"ROOT","domainid":1,"isextractable":false}, {"id":481,"name":"XEN Centos 5.4 x64 R2.0","displaytext":"XEN Centos 5.4 x64 R2.0","ispublic":true,"created":"2011-04-14T01:43:49+0000","isready":true,"passwordenabled":false,"format":"VHD","isfeatured":true,"crossZones":false,"ostypeid":12,"ostypename":"CentOS 5.3 (64-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":171966464000,"templatetype":"USER","hypervisor":"XenServer","domain":"ROOT","domainid":1,"isextractable":false}, {"id":421,"name":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","displaytext":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","ispublic":true,"created":"2011-03-22T02:54:06+0000","isready":true,"passwordenabled":false,"format":"VHD","isfeatured":true,"crossZones":false,"ostypeid":12,"ostypename":"CentOS 5.3 (64-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":167772160000,"templatetype":"USER","hypervisor":"XenServer","domain":"ROOT","domainid":1,"isextractable":false}, {"id":423,"name":"XEN Basic Centos 5.5 x64 PV r2.2","displaytext":"XEN Basic Centos 5.5 x64 PV r2.2","ispublic":true,"created":"2011-03-22T02:59:31+0000","isready":true,"passwordenabled":false,"format":"VHD","isfeatured":true,"crossZones":false,"ostypeid":12,"ostypename":"CentOS 5.3 (64-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":167772160000,"templatetype":"USER","hypervisor":"XenServer","domain":"ROOT","domainid":1,"isextractable":false}, {"id":422,"name":"XEN OpenSUSE x64 11.4 R2.0","displaytext":"XEN OpenSUSE x64 11.4 R2.0","ispublic":true,"created":"2011-03-22T02:58:25+0000","isready":true,"passwordenabled":false,"format":"VHD","isfeatured":true,"crossZones":false,"ostypeid":12,"ostypename":"CentOS 5.3 (64-bit)","account":"admin","zoneid":1,"zonename":"Sydney","size":171966464000,"templatetype":"USER","hypervisor":"XenServer","domain":"ROOT","domainid":1,"isextractable":false} ] } } diff --git test/compute/fixtures/cloudstack/listVirtualMachines_default.json test/compute/fixtures/cloudstack/listVirtualMachines_default.json new file mode 100644 index 0000000..d55c436 --- /dev/null +++ test/compute/fixtures/cloudstack/listVirtualMachines_default.json @@ -0,0 +1 @@ +{ "listvirtualmachinesresponse" : { "virtualmachine" : [ {"id":2600,"name":"test","displayname":"test","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","created":"2011-06-23T05:06:42+0000","state":"Running","haenable":false,"zoneid":1,"zonename":"Sydney","templateid":421,"templatename":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","templatedisplaytext":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","passwordenabled":false,"serviceofferingid":105,"serviceofferingname":"Compute Micro PRD","cpunumber":1,"cpuspeed":1200,"memory":384,"cpuused":"1.78%","networkkbsread":2,"networkkbswrite":2,"guestosid":12,"rootdeviceid":0,"rootdevicetype":"IscsiLUN","securitygroup":[],"nic":[{"id":3891,"networkid":860,"netmask":"255.255.240.0","gateway":"1.1.2.1","ipaddress":"1.1.1.116","traffictype":"Guest","type":"Virtual","isdefault":true}],"hypervisor":"XenServer"}, {"id":2601,"name":"test","displayname":"test","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","created":"2011-06-23T05:09:44+0000","state":"Starting","haenable":false,"zoneid":1,"zonename":"Sydney","templateid":421,"templatename":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","templatedisplaytext":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","passwordenabled":false,"serviceofferingid":105,"serviceofferingname":"Compute Micro PRD","cpunumber":1,"cpuspeed":1200,"memory":384,"guestosid":12,"rootdeviceid":0,"rootdevicetype":"IscsiLUN","securitygroup":[],"jobid":17147,"jobstatus":0,"nic":[{"id":3892,"networkid":860,"netmask":"255.255.240.0","gateway":"1.1.2.1","ipaddress":"1.1.1.203","traffictype":"Guest","type":"Virtual","isdefault":true}],"hypervisor":"XenServer"} ] } } diff --git test/compute/fixtures/cloudstack/listZones_default.json test/compute/fixtures/cloudstack/listZones_default.json new file mode 100644 index 0000000..0316936 --- /dev/null +++ test/compute/fixtures/cloudstack/listZones_default.json @@ -0,0 +1 @@ +{ "listzonesresponse" : { "zone" : [ {"id":1,"name":"Sydney","networktype":"Advanced","securitygroupsenabled":false} ] } } diff --git test/compute/fixtures/cloudstack/listZones_deployfail.json test/compute/fixtures/cloudstack/listZones_deployfail.json new file mode 100644 index 0000000..0316936 --- /dev/null +++ test/compute/fixtures/cloudstack/listZones_deployfail.json @@ -0,0 +1 @@ +{ "listzonesresponse" : { "zone" : [ {"id":1,"name":"Sydney","networktype":"Advanced","securitygroupsenabled":false} ] } } diff --git test/compute/fixtures/cloudstack/listZones_deployfail2.json test/compute/fixtures/cloudstack/listZones_deployfail2.json new file mode 100644 index 0000000..0316936 --- /dev/null +++ test/compute/fixtures/cloudstack/listZones_deployfail2.json @@ -0,0 +1 @@ +{ "listzonesresponse" : { "zone" : [ {"id":1,"name":"Sydney","networktype":"Advanced","securitygroupsenabled":false} ] } } diff --git test/compute/fixtures/cloudstack/queryAsyncJobResult_17164.json test/compute/fixtures/cloudstack/queryAsyncJobResult_17164.json new file mode 100644 index 0000000..31fd39e --- /dev/null +++ test/compute/fixtures/cloudstack/queryAsyncJobResult_17164.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":17164,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"virtualmachine":{"id":2602,"name":"fred","displayname":"fred","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","created":"2011-06-23T05:48:31+0000","state":"Running","haenable":false,"zoneid":1,"zonename":"Sydney","templateid":421,"templatename":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","templatedisplaytext":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","passwordenabled":false,"serviceofferingid":105,"serviceofferingname":"Compute Micro PRD","cpunumber":1,"cpuspeed":1200,"memory":384,"guestosid":12,"rootdeviceid":0,"rootdevicetype":"IscsiLUN","securitygroup":[],"nic":[{"id":3893,"networkid":860,"netmask":"255.255.240.0","gateway":"1.1.1.1","ipaddress":"1.1.1.2","traffictype":"Guest","type":"Virtual","isdefault":true}],"hypervisor":"XenServer"}}} } diff --git test/compute/fixtures/cloudstack/queryAsyncJobResult_17165.json test/compute/fixtures/cloudstack/queryAsyncJobResult_17165.json new file mode 100644 index 0000000..694d7d0 --- /dev/null +++ test/compute/fixtures/cloudstack/queryAsyncJobResult_17165.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":17165,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"virtualmachine":{"id":2602,"name":"fred","displayname":"fred","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","created":"2011-06-23T05:48:31+0000","state":"Running","haenable":false,"zoneid":1,"zonename":"Sydney","templateid":421,"templatename":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","templatedisplaytext":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","passwordenabled":false,"serviceofferingid":105,"serviceofferingname":"Compute Micro PRD","cpunumber":1,"cpuspeed":1200,"memory":384,"cpuused":"0.14%","networkkbsread":2,"networkkbswrite":1,"guestosid":12,"rootdeviceid":0,"rootdevicetype":"IscsiLUN","securitygroup":[],"nic":[{"id":3893,"networkid":860,"netmask":"255.255.240.0","gateway":"1.1.1.1","ipaddress":"1.1.1.2","traffictype":"Guest","type":"Virtual","isdefault":true}],"hypervisor":"XenServer"}}} } diff --git test/compute/fixtures/cloudstack/queryAsyncJobResult_17166.json test/compute/fixtures/cloudstack/queryAsyncJobResult_17166.json new file mode 100644 index 0000000..5245af2 --- /dev/null +++ test/compute/fixtures/cloudstack/queryAsyncJobResult_17166.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":17166,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"virtualmachine":{"id":2602,"name":"fred","displayname":"fred","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","created":"2011-06-23T05:48:31+0000","state":"Destroyed","haenable":false,"zoneid":1,"zonename":"Sydney","templateid":421,"templatename":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","templatedisplaytext":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","passwordenabled":false,"serviceofferingid":105,"serviceofferingname":"Compute Micro PRD","cpunumber":1,"cpuspeed":1200,"memory":384,"cpuused":"0.13%","networkkbsread":2,"networkkbswrite":1,"guestosid":12,"rootdeviceid":0,"rootdevicetype":"IscsiLUN","securitygroup":[],"nic":[{"id":3893,"networkid":860,"netmask":"255.255.240.0","gateway":"1.1.1.1","ipaddress":"1.1.1.2","traffictype":"Guest","type":"Virtual","isdefault":true}],"hypervisor":"XenServer"}}} } diff --git test/compute/fixtures/cloudstack/queryAsyncJobResult_17177.json test/compute/fixtures/cloudstack/queryAsyncJobResult_17177.json new file mode 100644 index 0000000..7b743c5 --- /dev/null +++ test/compute/fixtures/cloudstack/queryAsyncJobResult_17177.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":17177,"jobstatus":2} } diff --git test/compute/fixtures/cloudstack/rebootVirtualMachine_default.json test/compute/fixtures/cloudstack/rebootVirtualMachine_default.json new file mode 100644 index 0000000..07cdba3 --- /dev/null +++ test/compute/fixtures/cloudstack/rebootVirtualMachine_default.json @@ -0,0 +1 @@ +{ "rebootvirtualmachineresponse" : {"jobid":17165} } diff --git test/compute/test_cloudstack.py test/compute/test_cloudstack.py new file mode 100644 index 0000000..556d359 --- /dev/null +++ test/compute/test_cloudstack.py @@ -0,0 +1,88 @@ +import httplib +import sys +import unittest +import urlparse + +try: + import json +except: + import simplejson as json + +from libcloud.compute.drivers.cloudstack import CloudStackNodeDriver +from libcloud.compute.types import DeploymentError + +from test import MockHttpTestCase +from test.compute import TestCaseMixin +from test.file_fixtures import ComputeFileFixtures + +class CloudStackNodeDriverTest(unittest.TestCase, TestCaseMixin): + def setUp(self): + CloudStackNodeDriver.connectionCls.conn_classes = \ + (None, CloudStackMockHttp) + self.driver = CloudStackNodeDriver('apikey', 'secret') + self.driver.path = '/test/path' + self.driver.type = -1 + CloudStackMockHttp.fixture_tag = 'default' + + def test_create_node_immediate_failure(self): + size = self.driver.list_sizes()[0] + image = self.driver.list_images()[0] + CloudStackMockHttp.fixture_tag = 'deployfail' + try: + node = self.driver.create_node(name='node-name', + image=image, + size=size) + except: + return + self.assertTrue(False) + + def test_create_node_delayed_failure(self): + size = self.driver.list_sizes()[0] + image = self.driver.list_images()[0] + CloudStackMockHttp.fixture_tag = 'deployfail2' + try: + node = self.driver.create_node(name='node-name', + image=image, + size=size) + except: + return + self.assertTrue(False) + +class CloudStackMockHttp(MockHttpTestCase): + fixtures = ComputeFileFixtures('cloudstack') + fixture_tag = 'default' + + def _load_fixture(self, fixture): + body = self.fixtures.load(fixture) + return body, json.loads(body) + + def _test_path(self, method, url, body, headers): + url = urlparse.urlparse(url) + query = dict(urlparse.parse_qsl(url.query)) + + self.assertTrue('apiKey' in query) + self.assertTrue('command' in query) + self.assertTrue('response' in query) + self.assertTrue('signature' in query) + + self.assertTrue(query['response'] == 'json') + + del query['apiKey'] + del query['response'] + del query['signature'] + command = query.pop('command') + + if hasattr(self, '_cmd_' + command): + return getattr(self, '_cmd_' + command)(**query) + else: + fixture = command + '_' + self.fixture_tag + '.json' + body, obj = self._load_fixture(fixture) + return (httplib.OK, body, obj, httplib.responses[httplib.OK]) + + def _cmd_queryAsyncJobResult(self, jobid): + fixture = 'queryAsyncJobResult' + '_' + str(jobid) + '.json' + body, obj = self._load_fixture(fixture) + return (httplib.OK, body, obj, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git test/loadbalancer/fixtures/cloudstack/assignToLoadBalancerRule_default.json test/loadbalancer/fixtures/cloudstack/assignToLoadBalancerRule_default.json new file mode 100644 index 0000000..a2175dc --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/assignToLoadBalancerRule_default.json @@ -0,0 +1 @@ +{ "assigntoloadbalancerruleresponse" : {"jobid":17341} } diff --git test/loadbalancer/fixtures/cloudstack/associateIpAddress_default.json test/loadbalancer/fixtures/cloudstack/associateIpAddress_default.json new file mode 100644 index 0000000..a62dd4e --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/associateIpAddress_default.json @@ -0,0 +1 @@ +{ "associateipaddressresponse" : {"jobid":17346,"id":34000} } diff --git test/loadbalancer/fixtures/cloudstack/createLoadBalancerRule_default.json test/loadbalancer/fixtures/cloudstack/createLoadBalancerRule_default.json new file mode 100644 index 0000000..4a442a0 --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/createLoadBalancerRule_default.json @@ -0,0 +1 @@ +{ "createloadbalancerruleresponse" : { "loadbalancer" : {"id":2253,"name":"fake","publicipid":34000,"publicip":"1.1.1.49","publicport":"80","privateport":"80","algorithm":"roundrobin","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","state":"Add"} } } diff --git test/loadbalancer/fixtures/cloudstack/deleteLoadBalancerRule_default.json test/loadbalancer/fixtures/cloudstack/deleteLoadBalancerRule_default.json new file mode 100644 index 0000000..bec847a --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/deleteLoadBalancerRule_default.json @@ -0,0 +1 @@ +{ "deleteloadbalancerruleresponse" : {"jobid":17342} } diff --git test/loadbalancer/fixtures/cloudstack/disassociateIpAddress_default.json test/loadbalancer/fixtures/cloudstack/disassociateIpAddress_default.json new file mode 100644 index 0000000..eb1ec6b --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/disassociateIpAddress_default.json @@ -0,0 +1 @@ +{ "disassociateipaddressresponse" : {"jobid":17344} } diff --git test/loadbalancer/fixtures/cloudstack/listLoadBalancerRuleInstances_default.json test/loadbalancer/fixtures/cloudstack/listLoadBalancerRuleInstances_default.json new file mode 100644 index 0000000..912a268 --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/listLoadBalancerRuleInstances_default.json @@ -0,0 +1 @@ +{ "listloadbalancerruleinstancesresponse" : { "loadbalancerruleinstance" : [ {"id":2614,"name":"test_1308874974","displayname":"test_1308874974","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","created":"2011-06-24T00:22:56+0000","state":"Running","haenable":false,"zoneid":1,"zonename":"Sydney","templateid":421,"templatename":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","templatedisplaytext":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","passwordenabled":false,"serviceofferingid":105,"serviceofferingname":"Compute Micro PRD","cpunumber":1,"cpuspeed":1200,"memory":384,"cpuused":"0.14%","networkkbsread":2185,"networkkbswrite":109,"guestosid":12,"rootdeviceid":0,"rootdevicetype":"IscsiLUN","securitygroup":[],"nic":[{"id":3914,"networkid":860,"netmask":"255.255.240.0","gateway":"1.1.1.1","ipaddress":"1.1.3.122","traffictype":"Guest","type":"Virtual","isdefault":true}],"hypervisor":"XenServer"}, {"id":2615,"name":"test_1308875456","displayname":"test_1308875456","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","created":"2011-06-24T00:30:57+0000","state":"Running","haenable":false,"zoneid":1,"zonename":"Sydney","templateid":421,"templatename":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","templatedisplaytext":"XEN Basic Ubuntu 10.04 Server x64 PV r2.0","passwordenabled":false,"serviceofferingid":105,"serviceofferingname":"Compute Micro PRD","cpunumber":1,"cpuspeed":1200,"memory":384,"cpuused":"0.14%","networkkbsread":1118,"networkkbswrite":75,"guestosid":12,"rootdeviceid":0,"rootdevicetype":"IscsiLUN","securitygroup":[],"nic":[{"id":3915,"networkid":860,"netmask":"255.255.240.0","gateway":"1.1.1.1","ipaddress":"1.1.2.62","traffictype":"Guest","type":"Virtual","isdefault":true}],"hypervisor":"XenServer"} ] } } diff --git test/loadbalancer/fixtures/cloudstack/listLoadBalancerRules_default.json test/loadbalancer/fixtures/cloudstack/listLoadBalancerRules_default.json new file mode 100644 index 0000000..f3ae05a --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/listLoadBalancerRules_default.json @@ -0,0 +1 @@ +{ "listloadbalancerrulesresponse" : { "loadbalancerrule" : [ {"id":2247,"name":"test","publicipid":34000,"publicip":"1.1.1.49","publicport":"80","privateport":"80","algorithm":"roundrobin","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","state":"Active"},{"id":2249,"name":"testmore","publicipid":34001,"publicip":"1.1.2.49","publicport":"80","privateport":"80","algorithm":"leastconn","account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","state":"Active"} ] } } diff --git test/loadbalancer/fixtures/cloudstack/listZones_default.json test/loadbalancer/fixtures/cloudstack/listZones_default.json new file mode 100644 index 0000000..0316936 --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/listZones_default.json @@ -0,0 +1 @@ +{ "listzonesresponse" : { "zone" : [ {"id":1,"name":"Sydney","networktype":"Advanced","securitygroupsenabled":false} ] } } diff --git test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17340.json test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17340.json new file mode 100644 index 0000000..67a51fe --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17340.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":17340,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"success":true}} } diff --git test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17341.json test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17341.json new file mode 100644 index 0000000..6ea3b16 --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17341.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":17341,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"success":true}} } diff --git test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17342.json test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17342.json new file mode 100644 index 0000000..95c4e4a --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17342.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":17342,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"success":true}} } diff --git test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17344.json test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17344.json new file mode 100644 index 0000000..65d29c5 --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17344.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":17344,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"success":true}} } diff --git test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17346.json test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17346.json new file mode 100644 index 0000000..aac339b --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/queryAsyncJobResult_17346.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"jobid":17346,"jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"ipaddress":{"id":34000,"ipaddress":"1.1.1.49","allocated":"2011-06-24T05:52:55+0000","zoneid":1,"zonename":"Sydney","issourcenat":false,"account":"fakeaccount","domainid":801,"domain":"AA000062-libcloud-dev","forvirtualnetwork":true,"isstaticnat":false,"associatednetworkid":860,"networkid":200,"state":"Allocating"}}} } diff --git test/loadbalancer/fixtures/cloudstack/removeFromLoadBalancerRule_default.json test/loadbalancer/fixtures/cloudstack/removeFromLoadBalancerRule_default.json new file mode 100644 index 0000000..a70b5c5 --- /dev/null +++ test/loadbalancer/fixtures/cloudstack/removeFromLoadBalancerRule_default.json @@ -0,0 +1 @@ +{ "removefromloadbalancerruleresponse" : {"jobid":17340} } diff --git test/loadbalancer/test_cloudstack.py test/loadbalancer/test_cloudstack.py new file mode 100644 index 0000000..62433ac --- /dev/null +++ test/loadbalancer/test_cloudstack.py @@ -0,0 +1,95 @@ +import httplib +import sys +import unittest +import urlparse + +try: + import json +except: + import simplejson as json + +from libcloud.common.types import LibcloudError +from libcloud.loadbalancer.base import LoadBalancer, Member, Algorithm +from libcloud.loadbalancer.drivers.cloudstack import CloudStackLBDriver + +from test import MockHttpTestCase +from test.file_fixtures import LoadBalancerFileFixtures + +class CloudStackLBTests(unittest.TestCase): + def setUp(self): + CloudStackLBDriver.connectionCls.conn_classes = \ + (None, CloudStackMockHttp) + self.driver = CloudStackLBDriver('apikey', 'secret') + self.driver.path = '/test/path' + self.driver.type = -1 + self.driver.name = 'CloudStack' + CloudStackMockHttp.fixture_tag = 'default' + + def test_list_balancers(self): + balancers = self.driver.list_balancers() + for balancer in balancers: + self.assertTrue(isinstance(balancer, LoadBalancer)) + + def test_create_balancer(self): + members = [Member(1, '1.1.1.1', 80), Member(2, '1.1.1.2', 80)] + balancer = self.driver.create_balancer('fake', members) + self.assertTrue(isinstance(balancer, LoadBalancer)) + + def test_destroy_balancer(self): + balancer = self.driver.list_balancers()[0] + self.driver.destroy_balancer(balancer) + + def test_balancer_attach_member(self): + balancer = self.driver.list_balancers()[0] + member = Member(id=1234, ip='1.1.1.1', port=80) + balancer.attach_member(member) + + def test_balancer_detach_member(self): + balancer = self.driver.list_balancers()[0] + member = balancer.list_members()[0] + balancer.detach_member(member) + + def test_balancer_list_members(self): + balancer = self.driver.list_balancers()[0] + members = balancer.list_members() + for member in members: + self.assertTrue(isinstance(member, Member)) + +class CloudStackMockHttp(MockHttpTestCase): + fixtures = LoadBalancerFileFixtures('cloudstack') + fixture_tag = 'default' + + def _load_fixture(self, fixture): + body = self.fixtures.load(fixture) + return body, json.loads(body) + + def _test_path(self, method, url, body, headers): + url = urlparse.urlparse(url) + query = dict(urlparse.parse_qsl(url.query)) + + self.assertTrue('apiKey' in query) + self.assertTrue('command' in query) + self.assertTrue('response' in query) + self.assertTrue('signature' in query) + + self.assertTrue(query['response'] == 'json') + + del query['apiKey'] + del query['response'] + del query['signature'] + command = query.pop('command') + + if hasattr(self, '_cmd_' + command): + return getattr(self, '_cmd_' + command)(**query) + else: + fixture = command + '_' + self.fixture_tag + '.json' + body, obj = self._load_fixture(fixture) + return (httplib.OK, body, obj, httplib.responses[httplib.OK]) + + def _cmd_queryAsyncJobResult(self, jobid): + fixture = 'queryAsyncJobResult' + '_' + str(jobid) + '.json' + body, obj = self._load_fixture(fixture) + return (httplib.OK, body, obj, httplib.responses[httplib.OK]) + +if __name__ == "__main__": + sys.exit(unittest.main())