diff --git libcloud/compute/drivers/vcloud.py libcloud/compute/drivers/vcloud.py index 914d943..c194f7b 100644 --- libcloud/compute/drivers/vcloud.py +++ libcloud/compute/drivers/vcloud.py @@ -20,6 +20,7 @@ import sys import re import base64 import os +import urllib from libcloud.utils.py3 import httplib from libcloud.utils.py3 import urlparse from libcloud.utils.py3 import b @@ -85,7 +86,7 @@ class Vdc: self.storage = storage def __repr__(self): - return (('') + return ('' % (self.id, self.name, self.driver.name)) @@ -97,10 +98,43 @@ class Capacity: self.units = units def __repr__(self): - return (('') + return ('' % (self.limit, self.used, self.units)) +class ControlAccess: + """Represents control access settings of a node""" + + class AccessLevel: + READ_ONLY = 'ReadOnly' + CHANGE = 'Change' + FULL_CONTROL = 'FullControl' + + def __init__(self, node, everyone_access_level, subjects=None): + self.node = node + self.everyone_access_level = everyone_access_level + if not subjects: + subjects = [] + self.subjects = subjects + + def __repr__(self): + return ('' + % (self.node, self.everyone_access_level, self.subjects)) + + +class Subject: + """User or group subject""" + def __init__(self, type, name, access_level, id=None): + self.type = type + self.name = name + self.access_level = access_level + self.id = id + + def __repr__(self): + return ('' + % (self.type, self.name, self.access_level)) + + class InstantiateVAppXML(object): def __init__(self, name, template, net_href, cpus, memory, password=None, row=None, group=None): @@ -122,14 +156,14 @@ class InstantiateVAppXML(object): self.root = self._make_instantiation_root() self._add_vapp_template(self.root) - instantionation_params = ET.SubElement(self.root, + instantiation_params = ET.SubElement(self.root, "InstantiationParams") # product and virtual hardware - self._make_product_section(instantionation_params) - self._make_virtual_hardware(instantionation_params) + self._make_product_section(instantiation_params) + self._make_virtual_hardware(instantiation_params) - network_config_section = ET.SubElement(instantionation_params, + network_config_section = ET.SubElement(instantiation_params, "NetworkConfigSection") network_config = ET.SubElement(network_config_section, @@ -1047,6 +1081,145 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): res = self.connection.request(get_url_path(node.id)) return self._to_node(res.object) + def ex_get_control_access(self, node): + """ + Returns the control access settings for specified node. + + @param node: node to get the control access for + @type node: L{Node} + + @rtype: L{ControlAccess} + """ + res = self.connection.request( + '%s/controlAccess' % get_url_path(node.id)) + everyone_access_level = None + is_shared_elem = res.object.find( + fixxpath(res.object, "IsSharedToEveryone")) + if is_shared_elem is not None and is_shared_elem.text == 'true': + everyone_access_level = res.object.find( + fixxpath(res.object, "EveryoneAccessLevel")).text + + # Parse all subjects + subjects = [] + for elem in res.object.findall( + fixxpath(res.object, "AccessSettings/AccessSetting")): + access_level = elem.find(fixxpath(res.object, "AccessLevel")).text + subject_elem = elem.find(fixxpath(res.object, "Subject")) + if subject_elem.get('type') == 'application/vnd.vmware.admin.group+xml': + subj_type = 'group' + else: + subj_type = 'user' + res = self.connection.request(get_url_path(subject_elem.get('href'))) + name = res.object.get('name') + subject = Subject(type=subj_type, + name=name, + access_level=access_level, + id=subject_elem.get('href')) + subjects.append(subject) + + return ControlAccess(node, everyone_access_level, subjects) + + def ex_set_control_access(self, node, control_access): + """ + Sets control access for the specified node. + + @param node: node + @type node: L{Node} + + @param control_access: control access settings + @type control_access: L{ControlAccess} + + @rtype: C{None} + """ + xml = ET.Element('ControlAccessParams', + {'xmlns': 'http://www.vmware.com/vcloud/v1.5'}) + shared_to_everyone = ET.SubElement(xml, 'IsSharedToEveryone') + if control_access.everyone_access_level: + shared_to_everyone.text = 'true' + everyone_access_level = ET.SubElement(xml, 'EveryoneAccessLevel') + everyone_access_level.text = control_access.everyone_access_level + else: + shared_to_everyone.text = 'false' + + # Set subjects + if control_access.subjects: + access_settings_elem = ET.SubElement(xml, 'AccessSettings') + for subject in control_access.subjects: + setting = ET.SubElement(access_settings_elem, 'AccessSetting') + if subject.id: + href = subject.id + else: + res = self.ex_query(type=subject.type, filter='name==' + subject.name) + if not res: + raise LibcloudError('Specified subject "%s %s" not found ' + % (subject.type, subject.name)) + href = res[0]['href'] + ET.SubElement(setting, 'Subject', {'href': href}) + ET.SubElement(setting, 'AccessLevel').text = subject.access_level + + self.connection.request( + '%s/action/controlAccess' % get_url_path(node.id), + data=ET.tostring(xml), + headers={ + 'Content-Type': 'application/vnd.vmware.vcloud.controlAccess+xml' + }, + method='POST') + + def ex_query(self, type, filter=None, page=1, page_size=100, sort_asc=None, + sort_desc=None): + """ + Queries vCloud for specified type. See http://www.vmware.com/pdf/vcd_15_api_guide.pdf + for details. Each element of the returned list is a dictionary with all + attributes from the record. + + @param type: type to query (r.g. user, group, vApp etc.) + @type type: C{str} + + @param filter: filter expression (see documentation for syntax) + @type filter: C{str} + + @param page: page number + @type page: C{int} + + @param page_size: page size + @type page_size: C{int} + + @param sort_asc: sort in ascending order by specified field + @type sort_asc: C{str} + + @param sort_desc: sort in descending order by specified field + @type sort_desc: C{str} + + @rtype: C{list} of dict + """ + # This is a workaround for filter parameter encoding + # the urllib encodes (name==Developers%20Only) into + # %28name%3D%3DDevelopers%20Only%29) which is not accepted by vCloud + params = { + 'type': type, + 'pageSize': page_size, + 'page': page, + } + if sort_asc: + params['sortAsc'] = sort_asc + if sort_desc: + params['sortDesc'] = sort_desc + + url = '/api/query?' + urllib.urlencode(params) + if filter: + if not filter.startswith('('): + filter = '(' + filter + ')' + url += '&filter=' + filter.replace(' ', '+') + + results = [] + res = self.connection.request(url) + for elem in res.object: + if not elem.tag.endswith('Link'): + result = elem.attrib + result['type'] = elem.tag.split('}')[1] + results.append(result) + return results + def create_node(self, **kwargs): """Creates and returns node. If the source image is: - vApp template - a new vApp is instantiated from template @@ -1612,7 +1785,10 @@ class VCloud_1_5_NodeDriver(VCloudNodeDriver): 'name': vm_elem.get('name'), 'state': self.NODE_STATE_MAP[vm_elem.get('status')], 'public_ips': public_ips, - 'private_ips': private_ips + 'private_ips': private_ips, + 'os_type': vm_elem + .find('{http://schemas.dmtf.org/ovf/envelope/1}OperatingSystemSection') + .get('{http://www.vmware.com/schema/ovf}osType') } vms.append(vm) diff --git libcloud/test/compute/fixtures/vcloud_1_5/api_admin_group_b8202c48_7151_4e61_9a6c_155474c7d413.xml libcloud/test/compute/fixtures/vcloud_1_5/api_admin_group_b8202c48_7151_4e61_9a6c_155474c7d413.xml new file mode 100644 index 0000000..e3ec14d --- /dev/null +++ libcloud/test/compute/fixtures/vcloud_1_5/api_admin_group_b8202c48_7151_4e61_9a6c_155474c7d413.xml @@ -0,0 +1,11 @@ + + + + + \CF\CB\AD\5D\1D\34\09\4D\A4\77\8D\A3\CA\99\75\FB + + + + + + \ No newline at end of file diff --git libcloud/test/compute/fixtures/vcloud_1_5/api_query_group.xml libcloud/test/compute/fixtures/vcloud_1_5/api_query_group.xml new file mode 100644 index 0000000..bcbbd18 --- /dev/null +++ libcloud/test/compute/fixtures/vcloud_1_5/api_query_group.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git libcloud/test/compute/fixtures/vcloud_1_5/api_query_user.xml libcloud/test/compute/fixtures/vcloud_1_5/api_query_user.xml new file mode 100644 index 0000000..13c89db --- /dev/null +++ libcloud/test/compute/fixtures/vcloud_1_5/api_query_user.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git libcloud/test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_controlAccess.xml libcloud/test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_controlAccess.xml new file mode 100644 index 0000000..d6f426e --- /dev/null +++ libcloud/test/compute/fixtures/vcloud_1_5/api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_controlAccess.xml @@ -0,0 +1,10 @@ + + true + ReadOnly + + + + FullControl + + + \ No newline at end of file diff --git libcloud/test/compute/test_vcloud.py libcloud/test/compute/test_vcloud.py index a8deb9a..c2f5496 100644 --- libcloud/test/compute/test_vcloud.py +++ libcloud/test/compute/test_vcloud.py @@ -19,8 +19,8 @@ from xml.etree import ElementTree as ET from libcloud.utils.py3 import httplib, b -from libcloud.compute.drivers.vcloud import TerremarkDriver, VCloudNodeDriver -from libcloud.compute.drivers.vcloud import VCloud_1_5_NodeDriver, Vdc +from libcloud.compute.drivers.vcloud import TerremarkDriver, VCloudNodeDriver, Subject +from libcloud.compute.drivers.vcloud import VCloud_1_5_NodeDriver, ControlAccess from libcloud.compute.base import Node, NodeImage from libcloud.compute.types import NodeState @@ -139,6 +139,7 @@ class VCloud_1_5_Tests(unittest.TestCase, TestCaseMixin): 'state': NodeState.RUNNING, 'public_ips': ['65.41.67.2'], 'private_ips': ['65.41.67.2'], + 'os_type': 'rhel5_64Guest' }]}) node = ret[1] self.assertEqual(node.id, 'https://vm-vcloud/api/vApp/vapp-8c57a5b6-e61b-48ca-8a78-3b70ee65ef6b') @@ -153,6 +154,7 @@ class VCloud_1_5_Tests(unittest.TestCase, TestCaseMixin): 'state': NodeState.RUNNING, 'public_ips': ['192.168.0.103'], 'private_ips': ['192.168.0.100'], + 'os_type': 'rhel5_64Guest' }]}) def test_reboot_node(self): @@ -243,6 +245,30 @@ class VCloud_1_5_Tests(unittest.TestCase, TestCaseMixin): node = Node('https://vm-vcloud/api/vApp/vapp-8c57a5b6-e61b-48ca-8a78-3b70ee65ef6b', 'testNode', NodeState.RUNNING, [], [], self.driver) self.driver.ex_power_off_node(node) + def test_ex_query(self): + results = self.driver.ex_query('user', filter='name==jrambo', page=2, page_size=30, sort_desc='startDate') + self.assertEqual(len(results), 1) + self.assertEqual(results[0]['type'], 'UserRecord') + self.assertEqual(results[0]['name'], 'jrambo') + self.assertEqual(results[0]['isLdapUser'], 'true') + + def test_ex_get_control_access(self): + node = Node('https://vm-vcloud/api/vApp/vapp-8c57a5b6-e61b-48ca-8a78-3b70ee65ef6b', 'testNode', NodeState.RUNNING, [], [], self.driver) + control_access = self.driver.ex_get_control_access(node) + self.assertEqual(control_access.everyone_access_level, ControlAccess.AccessLevel.READ_ONLY) + self.assertEqual(len(control_access.subjects), 1) + self.assertEqual(control_access.subjects[0].type, 'group') + self.assertEqual(control_access.subjects[0].name, 'MyGroup') + self.assertEqual(control_access.subjects[0].id, 'https://vm-vcloud/api/admin/group/b8202c48-7151-4e61-9a6c-155474c7d413') + self.assertEqual(control_access.subjects[0].access_level, ControlAccess.AccessLevel.FULL_CONTROL) + + def test_ex_set_control_access(self): + node = Node('https://vm-vcloud/api/vApp/vapp-8c57a5b6-e61b-48ca-8a78-3b70ee65ef6b', 'testNode', NodeState.RUNNING, [], [], self.driver) + control_access = ControlAccess(node, None, [Subject( + name = 'MyGroup', + type = 'group', + access_level = ControlAccess.AccessLevel.FULL_CONTROL)]) + self.driver.ex_set_control_access(node, control_access) class TerremarkMockHttp(MockHttp): @@ -464,5 +490,34 @@ class VCloud_1_5_MockHttp(MockHttp): body = self.fixtures.load('api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_power_action_all.xml') return httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED] + def _api_query(self, method, url, body, headers): + assert method == 'GET' + if 'type=user' in url: + assert 'page=2' in url + assert 'filter=(name==jrambo)' in url + assert 'sortDesc=startDate' + body = self.fixtures.load('api_query_user.xml') + elif 'type=group' in url: + body = self.fixtures.load('api_query_group.xml') + else: + raise AssertionError('Unexpected query type') + return httplib.OK, body, headers, httplib.responses[httplib.OK] + + def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_controlAccess(self, method, url, body, headers): + body = self.fixtures.load('api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_controlAccess.xml') + return httplib.OK, body, headers, httplib.responses[httplib.OK] + + def _api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6b_action_controlAccess(self, method, url, body, headers): + assert method == 'POST' + assert 'false' in body + assert '' in body + assert 'FullControl' in body + body = self.fixtures.load('api_vApp_vapp_8c57a5b6_e61b_48ca_8a78_3b70ee65ef6a_controlAccess.xml') + return httplib.OK, body, headers, httplib.responses[httplib.OK] + + def _api_admin_group_b8202c48_7151_4e61_9a6c_155474c7d413(self, method, url, body, headers): + body = self.fixtures.load('api_admin_group_b8202c48_7151_4e61_9a6c_155474c7d413.xml') + return httplib.OK, body, headers, httplib.responses[httplib.OK] + if __name__ == '__main__': sys.exit(unittest.main())