Add CloudStackProject class. Add option to select project and disk offering on node creation.
Closes #257. Signed-off-by: Tomaz Muraus <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/583bfba6 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/583bfba6 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/583bfba6 Branch: refs/heads/trunk Commit: 583bfba634a74c0f1c43a987de44e4b9f6bd8e7b Parents: aa509b0 Author: Jim Divine <[email protected]> Authored: Wed Mar 5 13:39:28 2014 -0600 Committer: Tomaz Muraus <[email protected]> Committed: Fri Mar 7 22:45:14 2014 +0100 ---------------------------------------------------------------------- libcloud/compute/drivers/cloudstack.py | 159 +++++++++++++++++-- .../deployVirtualMachine_deployproject.json | 1 + .../cloudstack/listProjects_default.json | 49 ++++++ .../cloudstack/queryAsyncJobResult_11117.json | 63 ++++++++ libcloud/test/compute/test_cloudstack.py | 33 ++++ 5 files changed, 291 insertions(+), 14 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/583bfba6/libcloud/compute/drivers/cloudstack.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/cloudstack.py b/libcloud/compute/drivers/cloudstack.py index 5d66e8d..83e09b8 100644 --- a/libcloud/compute/drivers/cloudstack.py +++ b/libcloud/compute/drivers/cloudstack.py @@ -157,6 +157,14 @@ RESOURCE_EXTRA_ATTRIBUTES_MAP = { 'hypervisor': { 'key_name': 'hypervisor', 'transform_func': str + }, + 'project': { + 'key_name': 'project', + 'transform_func': str + }, + 'project_id': { + 'key_name': 'projectid', + 'transform_func': str } }, 'volume': { @@ -188,6 +196,58 @@ RESOURCE_EXTRA_ATTRIBUTES_MAP = { 'key_name': 'zonename', 'transform_func': str } + }, + 'project': { + 'account': {'key_name': 'account', 'transform_func': str}, + 'cpuavailable': {'key_name': 'cpuavailable', 'transform_func': int}, + 'cpulimit': {'key_name': 'cpulimit', 'transform_func': int}, + 'cputotal': {'key_name': 'cputotal', 'transform_func': int}, + 'domain': {'key_name': 'domain', 'transform_func': str}, + 'domainid': {'key_name': 'domainid', 'transform_func': str}, + 'ipavailable': {'key_name': 'ipavailable', 'transform_func': int}, + 'iplimit': {'key_name': 'iplimit', 'transform_func': int}, + 'iptotal': {'key_name': 'iptotal', 'transform_func': int}, + 'memoryavailable': {'key_name': 'memoryavailable', + 'transform_func': int}, + 'memorylimit': {'key_name': 'memorylimit', 'transform_func': int}, + 'memorytotal': {'key_name': 'memorytotal', 'transform_func': int}, + 'networkavailable': {'key_name': 'networkavailable', + 'transform_func': int}, + 'networklimit': {'key_name': 'networklimit', 'transform_func': int}, + 'networktotal': {'key_name': 'networktotal', 'transform_func': int}, + 'primarystorageavailable': {'key_name': 'primarystorageavailable', + 'transform_func': int}, + 'primarystoragelimit': {'key_name': 'primarystoragelimit', + 'transform_func': int}, + 'primarystoragetotal': {'key_name': 'primarystoragetotal', + 'transform_func': int}, + 'secondarystorageavailable': {'key_name': 'secondarystorageavailable', + 'transform_func': int}, + 'secondarystoragelimit': {'key_name': 'secondarystoragelimit', + 'transform_func': int}, + 'secondarystoragetotal': {'key_name': 'secondarystoragetotal', + 'transform_func': int}, + 'snapshotavailable': {'key_name': 'snapshotavailable', + 'transform_func': int}, + 'snapshotlimit': {'key_name': 'snapshotlimit', 'transform_func': int}, + 'snapshottotal': {'key_name': 'snapshottotal', 'transform_func': int}, + 'state': {'key_name': 'state', 'transform_func': str}, + 'tags': {'key_name': 'tags', 'transform_func': str}, + 'templateavailable': {'key_name': 'templateavailable', + 'transform_func': int}, + 'templatelimit': {'key_name': 'templatelimit', 'transform_func': int}, + 'templatetotal': {'key_name': 'templatetotal', 'transform_func': int}, + 'vmavailable': {'key_name': 'vmavailable', 'transform_func': int}, + 'vmlimit': {'key_name': 'vmlimit', 'transform_func': int}, + 'vmrunning': {'key_name': 'vmrunning', 'transform_func': int}, + 'vmtotal': {'key_name': 'vmtotal', 'transform_func': int}, + 'volumeavailable': {'key_name': 'volumeavailable', + 'transform_func': int}, + 'volumelimit': {'key_name': 'volumelimit', 'transform_func': int}, + 'volumetotal': {'key_name': 'volumetotal', 'transform_func': int}, + 'vpcavailable': {'key_name': 'vpcavailable', 'transform_func': int}, + 'vpclimit': {'key_name': 'vpclimit', 'transform_func': int}, + 'vpctotal': {'key_name': 'vpctotal', 'transform_func': int} } } @@ -402,6 +462,25 @@ class CloudStackNetwork(object): self.networkofferingid, self.zoneid, self.driver.name)) +class CloudStackProject(object): + """ + Class representing a CloudStack Project. + """ + + def __init__(self, id, name, display_text, driver, extra=None): + self.id = id + self.name = name + self.display_text = display_text + self.driver = driver + self.extra = extra or {} + + def __repr__(self): + return (('<CloudStackProject: id=%s, display_text=%s, name=%s, ' + 'driver=%s>') + % (self.id, self.display_text, self.name, + self.driver.name)) + + class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): """ Driver for the CloudStack API. @@ -516,13 +595,22 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): return locations - def list_nodes(self): + def list_nodes(self, project=None): """ @inherits: :class:`NodeDriver.list_nodes` + + :keyword project: Limit nodes returned to those configured under + the defined project. + :type project: :class:`.CloudStackProject` + :rtype: ``list`` of :class:`CloudStackNode` """ - vms = self._sync_request('listVirtualMachines') - addrs = self._sync_request('listPublicIpAddresses') + + args = {} + if project: + args['projectid'] = project.id + vms = self._sync_request('listVirtualMachines', params=args) + addrs = self._sync_request('listPublicIpAddresses', params=args) public_ips_map = {} for addr in addrs.get('publicipaddress', []): @@ -604,6 +692,13 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): into. :type networks: ``list`` of :class:`.CloudStackNetwork` + :keyword project: Optional project to create the new node under. + :type project: :class:`.CloudStackProject` + + :keyword diskoffering: Optional disk offering to add to the new + node. + :type diskoffering: :class:`.CloudStackDiskOffering` + :keyword ex_keyname: Name of existing keypair :type ex_keyname: ``str`` @@ -636,6 +731,8 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): image = kwargs.get('image', None) location = kwargs.get('location', None) networks = kwargs.get('networks', None) + project = kwargs.get('project', None) + diskoffering = kwargs.get('diskoffering', None) ex_key_name = kwargs.get('ex_keyname', None) ex_user_data = kwargs.get('ex_userdata', None) ex_security_groups = kwargs.get('ex_security_groups', None) @@ -659,6 +756,12 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): networks = ','.join([network.id for network in networks]) server_params['networkids'] = networks + if project: + server_params['projectid'] = project.id + + if diskoffering: + server_params['diskofferingid'] = diskoffering.id + if ex_key_name: server_params['keypair'] = ex_key_name @@ -771,16 +874,44 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): extra['tags'] = self._get_resource_tags(net['tags']) networks.append(CloudStackNetwork( - net['displaytext'], - net['name'], - net['networkofferingid'], - net['id'], - net['zoneid'], - self, - extra=extra)) + net['displaytext'], + net['name'], + net['networkofferingid'], + net['id'], + net['zoneid'], + self, + extra=extra)) return networks + def ex_list_projects(self): + """ + List the available projects + + :rtype ``list`` of :class:`CloudStackProject` + """ + + res = self._sync_request(command='listProjects', + method='GET') + projs = res.get('project', []) + + projects = [] + extra_map = RESOURCE_EXTRA_ATTRIBUTES_MAP['project'] + for proj in projs: + extra = self._get_extra_dict(proj, extra_map) + + if 'tags' in proj: + extra['tags'] = self._get_resource_tags(proj['tags']) + + projects.append(CloudStackProject( + id=proj['id'], + name=proj['name'], + display_text=proj['displaytext'], + driver=self, + extra=extra)) + + return projects + def create_volume(self, size, name, location=None, snapshot=None): """ Creates a data volume @@ -873,10 +1004,10 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): extra['tags'] = self._get_resource_tags(vol['tags']) list_volumes.append(StorageVolume(id=vol['id'], - name=vol['name'], - size=vol['size'], - driver=self, - extra=extra)) + name=vol['name'], + size=vol['size'], + driver=self, + extra=extra)) return list_volumes def list_key_pairs(self, **kwargs): http://git-wip-us.apache.org/repos/asf/libcloud/blob/583bfba6/libcloud/test/compute/fixtures/cloudstack/deployVirtualMachine_deployproject.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/deployVirtualMachine_deployproject.json b/libcloud/test/compute/fixtures/cloudstack/deployVirtualMachine_deployproject.json new file mode 100644 index 0000000..bf7b25e --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/deployVirtualMachine_deployproject.json @@ -0,0 +1 @@ +{ "deployvirtualmachineresponse" : {"jobid":11117,"id":"19253fbf-abb7-4013-a8a1-97df3b93f206"} } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/583bfba6/libcloud/test/compute/fixtures/cloudstack/listProjects_default.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/listProjects_default.json b/libcloud/test/compute/fixtures/cloudstack/listProjects_default.json new file mode 100644 index 0000000..b100edf --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/listProjects_default.json @@ -0,0 +1,49 @@ +{ "listprojectsresponse": { + "count": 1, + "project": [ + { + "id": "e7ce14a8-abb7-48b9-931f-5f426febed6d", + "name": "ExampleProjectName", + "displaytext": "ExampleProject", + "account": "CB0512", + "cpuavailable": 18, + "cpulimit": 32, + "cputotal": 14, + "domain": "ExampleDomain", + "domainid": "dc0314d4-09aa-4e8f-8a54-419ecf344635", + "ipavailable": 8, + "iplimit": 8, + "iptotal": 0, + "memoryavailable": 72000, + "memorylimit": 128000, + "memorytotal": 56000, + "networkavailable": 0, + "networklimit": 0, + "networktotal": 0, + "primarystorageavailable": 1204, + "primarystoragelimit": 1600, + "primarystoragetotal": 396, + "secondarystorageavailable": 3817, + "secondarystoragelimit": 4000, + "secondarystoragetotal": 183, + "snapshotavailable": 17, + "snapshotlimit": 20, + "snapshottotal": 3, + "state": "Active", + "tags": [], + "templateavailable": 17, + "templatelimit": 20, + "templatetotal": 3, + "vmavailable": 1, + "vmlimit": 8, + "vmrunning": 7, + "vmtotal": 7, + "volumeavailable": 1, + "volumelimit": 16, + "volumetotal": 15, + "vpcavailable": 20, + "vpclimit": 0, + "vpctotal": 0 + } + ] +} } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/583bfba6/libcloud/test/compute/fixtures/cloudstack/queryAsyncJobResult_11117.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/queryAsyncJobResult_11117.json b/libcloud/test/compute/fixtures/cloudstack/queryAsyncJobResult_11117.json new file mode 100644 index 0000000..8f7c76a --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/queryAsyncJobResult_11117.json @@ -0,0 +1,63 @@ +{ "queryasyncjobresultresponse": { + "accountid": "86d47ca2-726b-4b85-a18a-77d6b0d79829", + "userid": "20cd68f5-0633-48a5-826e-e4e2a00dd6b8", + "cmd": "org.apache.cloudstack.api.command.user.vm.DeployVMCmd", + "jobstatus": 1, + "jobprocstatus": 0, + "jobresultcode": 0, + "jobresulttype": "object", + "jobresult": { + "virtualmachine": { + "id": "19253fbf-abb7-4013-a8a1-97df3b93f206", + "name": "TestNode", + "projectid": "b90442d1-079b-4066-ab7d-41f8f3a5078b", + "project": "Test Project", + "domainid": "dc0314d4-09aa-4e8f-8a54-419ecf344635", + "domain": "Test Domain", + "created": "2014-03-06T15:39:44-0600", + "state": "Running", + "haenable": false, + "zoneid": "d630b15a-a9e1-4641-bee8-355005b7a14d", + "zonename": "TestZone", + "templateid": "a032e8a0-3411-48b7-9e78-ff66823e6561", + "templatename": "OL-6.3.1-64-13.11.01", + "templatedisplaytext": "OL-6.3.1-64-13.11.01", + "passwordenabled": true, + "serviceofferingid": "519f8667-26d0-40e5-a1cd-da04be1fd9b5", + "serviceofferingname": "Test Service Offering", + "cpunumber": 1, + "cpuspeed": 2000, + "memory": 2000, + "guestosid": "b8506c91-6d8e-4086-8659-f6296a7b71ac", + "rootdeviceid": 0, + "rootdevicetype": "ROOT", + "securitygroup": [], + "password": "mW6crjxag", + "nic": [ + { + "id": "1c144283-979a-4359-b695-3334dc403457", + "networkid": "1bf4acce-19a5-4830-ab1d-444f8acb9986", + "networkname": "Public", + "netmask": "255.255.252.0", + "gateway": "10.1.2.2", + "ipaddress": "10.2.2.8", + "isolationuri": "vlan://2950", + "broadcasturi": "vlan://2950", + "traffictype": "Guest", + "type": "Shared", + "isdefault": true, + "macaddress": "06:ef:30:00:04:22" + } + ], + "hypervisor": "VMware", + "tags": [], + "affinitygroup": [], + "displayvm": true, + "isdynamicallyscalable": false, + "jobid": "e23b9f0c-b7ae-4ffe-aea0-c9cf436cc315", + "jobstatus": 0 + } + }, + "created": "2014-03-06T15:39:44-0600", + "jobid": "e23b9f0c-b7ae-4ffe-aea0-c9cf436cc315" +} } http://git-wip-us.apache.org/repos/asf/libcloud/blob/583bfba6/libcloud/test/compute/test_cloudstack.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/test_cloudstack.py b/libcloud/test/compute/test_cloudstack.py index fc73116..b90c905 100644 --- a/libcloud/test/compute/test_cloudstack.py +++ b/libcloud/test/compute/test_cloudstack.py @@ -135,6 +135,20 @@ class CloudStackCommonTestCase(TestCaseMixin): self.assertEqual(node.name, 'test') self.assertEqual(node.extra['key_name'], 'foobar') + def test_create_node_project(self): + size = self.driver.list_sizes()[0] + image = self.driver.list_images()[0] + location = self.driver.list_locations()[0] + project = self.driver.ex_list_projects()[0] + CloudStackMockHttp.fixture_tag = 'deployproject' + node = self.driver.create_node(name='test', + location=location, + image=image, + size=size, + project=project) + self.assertEqual(node.name, 'TestNode') + self.assertEqual(node.extra['project'], 'Test Project') + def test_list_images_no_images_available(self): CloudStackMockHttp.fixture_tag = 'notemplates' @@ -182,6 +196,25 @@ class CloudStackCommonTestCase(TestCaseMixin): fixture_networks[i]['networkofferingid']) self.assertEqual(network.zoneid, fixture_networks[i]['zoneid']) + def test_ex_list_projects(self): + _, fixture = CloudStackMockHttp()._load_fixture( + 'listProjects_default.json') + fixture_projects = fixture['listprojectsresponse']['project'] + + projects = self.driver.ex_list_projects() + + for i, project in enumerate(projects): + self.assertEqual(project.id, fixture_projects[i]['id']) + self.assertEqual( + project.display_text, fixture_projects[i]['displaytext']) + self.assertEqual(project.name, fixture_projects[i]['name']) + self.assertEqual( + project.extra['domainid'], + fixture_projects[i]['domainid']) + self.assertEqual( + project.extra['cpulimit'], + fixture_projects[i]['cpulimit']) + def test_create_volume(self): volumeName = 'vol-0' location = self.driver.list_locations()[0]
