diff --git a/libcloud/common/dimensiondata.py b/libcloud/common/dimensiondata.py index ee7d83197b..ce8e39e605 100644 --- a/libcloud/common/dimensiondata.py +++ b/libcloud/common/dimensiondata.py @@ -434,6 +434,50 @@ def request_with_orgId_api_2(self, action, params=None, data='', params=params, data=data, method=method, headers=headers) + def paginated_request_with_orgId_api_2(self, action, params=None, data='', + headers=None, method='GET', + page_size=250): + """ + A paginated request to the MCP2.0 API + This essentially calls out to request_with_orgId_api_2 for each page + and yields the response to make a generator + This generator can be looped through to grab all the pages. + + :param action: The resource to access (i.e. 'network/vlan') + :type action: ``str`` + + :param params: Parameters to give to the action + :type params: ``dict`` or ``None`` + + :param data: The data payload to be added to the request + :type data: ``str`` + + :param headers: Additional header to be added to the request + :type headers: ``str`` or ``dict`` or ``None`` + + :param method: HTTP Method for the request (i.e. 'GET', 'POST') + :type method: ``str`` + + :param page_size: The size of each page to be returned + Note: Max page size in MCP2.0 is currently 250 + :type page_size: ``int`` + """ + if params is None: + params = {} + params['pageSize'] = page_size + + paged_resp = self.request_with_orgId_api_2(action, params, + data, headers, + method).object + yield paged_resp + + while paged_resp.get('pageCount') >= paged_resp.get('pageSize'): + params['pageNumber'] = int(paged_resp.get('pageNumber')) + 1 + paged_resp = self.request_with_orgId_api_2(action, params, + data, headers, + method).object + yield paged_resp + def get_resource_path_api_1(self): """ This method returns a resource path which is necessary for referencing @@ -731,6 +775,31 @@ def __repr__(self): % (self.id, self.status)) +class DimensionDataAntiAffinityRule(object): + """ + Anti-Affinity rule for DimensionData + + An Anti-Affinity rule ensures that servers in the rule will + not reside on the same VMware ESX host. + """ + def __init__(self, id, node_list): + """ + Instantiate a new :class:`DimensionDataAntiAffinityRule` + + :param id: The ID of the Anti-Affinity rule + :type id: ``str`` + + :param node_list: List of node ids that belong in this rule + :type node_list: ``list`` of ``str`` + """ + self.id = id + self.node_list = node_list + + def __repr__(self): + return (('') + % (self.id)) + + class DimensionDataVlan(object): """ DimensionData VLAN. diff --git a/libcloud/compute/drivers/dimensiondata.py b/libcloud/compute/drivers/dimensiondata.py index 6762e3657f..05c6482f92 100644 --- a/libcloud/compute/drivers/dimensiondata.py +++ b/libcloud/compute/drivers/dimensiondata.py @@ -36,6 +36,7 @@ from libcloud.common.dimensiondata import DimensionDataFirewallRule from libcloud.common.dimensiondata import DimensionDataFirewallAddress from libcloud.common.dimensiondata import DimensionDataNatRule +from libcloud.common.dimensiondata import DimensionDataAntiAffinityRule from libcloud.common.dimensiondata import NetworkDomainServicePlan from libcloud.common.dimensiondata import API_ENDPOINTS, DEFAULT_REGION from libcloud.common.dimensiondata import TYPES_URN @@ -686,6 +687,112 @@ def ex_update_node(self, node, name=None, description=None, response_code = findtext(body, 'result', GENERAL_NS) return response_code in ['IN_PROGRESS', 'SUCCESS'] + def ex_create_anti_affinity_rule(self, node_list): + """ + Create an anti affinity rule given a list of nodes + Anti affinity rules ensure that servers will not reside + on the same VMware ESX host + + :param node_list: The list of nodes to create a rule for + :type node_list: ``list`` of :class:`Node` or + ``list`` of ``str`` + + :rtype: ``bool`` + """ + if not isinstance(node_list, (list, tuple)): + raise TypeError("Node list must be a list or a tuple.") + anti_affinity_xml_request = ET.Element('NewAntiAffinityRule', + {'xmlns': SERVER_NS}) + for node in node_list: + ET.SubElement(anti_affinity_xml_request, 'serverId').text = \ + self._node_to_node_id(node) + result = self.connection.request_with_orgId_api_1( + 'antiAffinityRule', + method='POST', + data=ET.tostring(anti_affinity_xml_request)).object + response_code = findtext(result, 'result', GENERAL_NS) + return response_code in ['IN_PROGRESS', 'SUCCESS'] + + def ex_delete_anti_affinity_rule(self, anti_affinity_rule): + """ + Remove anti affinity rule + + :param anti_affinity_rule: The anti affinity rule to delete + :type anti_affinity_rule: :class:`DimensionDataAntiAffinityRule` or + ``str`` + + :rtype: ``bool`` + """ + rule_id = self._anti_affinity_rule_to_anti_affinity_rule_id( + anti_affinity_rule) + result = self.connection.request_with_orgId_api_1( + 'antiAffinityRule/%s?delete' % (rule_id), + method='GET').object + response_code = findtext(result, 'result', GENERAL_NS) + return response_code in ['IN_PROGRESS', 'SUCCESS'] + + def ex_list_anti_affinity_rules(self, network=None, network_domain=None, + node=None, filter_id=None, + filter_state=None): + """ + List anti affinity rules for a network, network domain, or node + + :param network: The network to list anti affinity rules for + One of network, network_domain, or node is required + :type network: :class:`DimensionDataNetwork` or ``str`` + + :param network_domain: The network domain to list anti affinity rules + One of network, network_domain, + or node is required + :type network_domain: :class:`DimensionDataNetworkDomain` or ``str`` + + :param node: The node to list anti affinity rules for + One of network, netwok_domain, or node is required + :type node: :class:`Node` or ``str`` + + :param filter_id: This will allow you to filter the rules + by this node id + :type filter_id: ``str`` + + :type filter_state: This will allow you to filter rules by + node state (i.e. NORMAL) + :type filter_state: ``str`` + + :rtype: ``list`` of :class:`DimensionDataAntiAffinityRule` + """ + not_none_arguments = [key + for key in (network, network_domain, node) + if key is not None] + if len(not_none_arguments) != 1: + raise ValueError("One and ONLY one of network, " + "network_domain, or node must be set") + + params = {} + if network_domain is not None: + params['networkDomainId'] = \ + self._network_domain_to_network_domain_id(network_domain) + if network is not None: + params['networkId'] = \ + self._network_to_network_id(network) + if node is not None: + params['serverId'] = \ + self._node_to_node_id(node) + if filter_id is not None: + params['id'] = filter_id + if filter_state is not None: + params['state'] = filter_state + + paged_result = self.connection.paginated_request_with_orgId_api_2( + 'server/antiAffinityRule', + method='GET', + params=params + ) + + rules = [] + for result in paged_result: + rules.extend(self._to_anti_affinity_rules(result)) + return rules + def ex_attach_node_to_vlan(self, node, vlan): """ Attach a node to a VLAN by adding an additional NIC to @@ -1205,9 +1312,38 @@ def ex_list_firewall_rules(self, network_domain, page_size=50, params=params).object return self._to_firewall_rules(response, network_domain) - def ex_create_firewall_rule(self, network_domain, rule, position): + def ex_create_firewall_rule(self, network_domain, rule, position, + position_relative_to_rule=None): + """ + Creates a firewall rule + + :param network_domain: The network domain in which to create + the firewall rule + :type network_domain: :class:`DimensionDataNetworkDomain` or ``str`` + + :param rule: The rule in which to create + :type rule: :class:`DimensionDataFirewallRule` + + :param position: The position in which to create the rule + There are two types of positions + with position_relative_to_rule arg and without it + With: 'BEFORE' or 'AFTER' + Without: 'FIRST' or 'LAST' + :type position: ``str`` + + :param position_relative_to_rule: The rule or rule name in + which to decide positioning by + :type position_relative_to_rule: + :class:`DimensionDataFirewallRule` or ``str`` + + :rtype: ``bool`` + """ + positions_without_rule = ('FIRST', 'LAST') + positions_with_rule = ('BEFORE', 'AFTER') + create_node = ET.Element('createFirewallRule', {'xmlns': TYPES_URN}) - ET.SubElement(create_node, "networkDomainId").text = network_domain.id + ET.SubElement(create_node, "networkDomainId").text = \ + self._network_domain_to_network_domain_id(network_domain) ET.SubElement(create_node, "name").text = rule.name ET.SubElement(create_node, "action").text = rule.action ET.SubElement(create_node, "ipVersion").text = rule.ip_version @@ -1240,8 +1376,26 @@ def ex_create_firewall_rule(self, network_domain, rule, position): dest_port.set('begin', rule.destination.port_begin) if rule.destination.port_end is not None: dest_port.set('end', rule.destination.port_end) - ET.SubElement(create_node, "enabled").text = 'true' + ET.SubElement(create_node, "enabled").text = rule.enabled + + # Set up positioning of rule placement = ET.SubElement(create_node, "placement") + if position_relative_to_rule is not None: + if position not in positions_with_rule: + raise ValueError("When position_relative_to_rule is specified" + " position must be %s" + % ', '.join(positions_with_rule)) + if isinstance(position_relative_to_rule, + DimensionDataFirewallRule): + rule_name = position_relative_to_rule.name + else: + rule_name = position_relative_to_rule + placement.set('relativeToRule', rule_name) + else: + if position not in positions_without_rule: + raise ValueError("When position_relative_to_rule is not" + " specified position must be %s" + % ', '.join(positions_without_rule)) placement.set('position', position) response = self.connection.request_with_orgId_api_2( @@ -1782,6 +1936,22 @@ def _to_nat_rule(self, element, network_domain): external_ip=findtext(element, 'externalIp', TYPES_URN), status=findtext(element, 'state', TYPES_URN)) + def _to_anti_affinity_rules(self, object): + rules = [] + for element in findall(object, 'antiAffinityRule', TYPES_URN): + rules.append( + self._to_anti_affinity_rule(element)) + return rules + + def _to_anti_affinity_rule(self, element): + node_list = [] + for node in findall(element, 'serverSummary', TYPES_URN): + node_list.append(node.get('id')) + return DimensionDataAntiAffinityRule( + id=element.get('id'), + node_list=node_list + ) + def _to_firewall_rules(self, object, network_domain): rules = [] locations = self.list_locations() @@ -2089,6 +2259,10 @@ def _get_node_state(state, started, action): else: return NodeState.TERMINATED + @staticmethod + def _node_to_node_id(node): + return dd_object_to_id(node, Node) + @staticmethod def _location_to_location_id(location): return dd_object_to_id(location, NodeLocation) @@ -2105,6 +2279,10 @@ def _image_to_image_id(image): def _network_to_network_id(network): return dd_object_to_id(network, DimensionDataNetwork) + @staticmethod + def _anti_affinity_rule_to_anti_affinity_rule_id(rule): + return dd_object_to_id(rule, DimensionDataAntiAffinityRule) + @staticmethod def _network_domain_to_network_domain_id(network_domain): return dd_object_to_id(network_domain, DimensionDataNetworkDomain) diff --git a/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_network_firewallRule.xml b/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_network_firewallRule.xml index fb5b6ed94b..536b350e18 100644 --- a/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_network_firewallRule.xml +++ b/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_network_firewallRule.xml @@ -214,12 +214,60 @@ IPV6 TCP - + true NORMAL + + + 423c4386-87b4-43c4-9604-88ae237bfc7f + RULE_WITH_SOURCE_AND_DEST_IP_ONLY + ACCEPT_DECISIVELY + IPV4 + TCP + + + + + + + true + NORMAL + + + 423c4386-87b4-43c4-9604-88ae237bfc7f + RULE_WITH_DEST_IP_NO_PORT + ACCEPT_DECISIVELY + IPV4 + TCP + + + + + + + + true + NORMAL + + + 423c4386-87b4-43c4-9604-88ae237bfc7f + RULE_WITH_SOURCE_AND_DEST + ACCEPT_DECISIVELY + IPV4 + TCP + + + + + + + + + true + NORMAL - \ No newline at end of file + diff --git a/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list.xml b/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list.xml new file mode 100644 index 0000000000..290b7e4010 --- /dev/null +++ b/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list.xml @@ -0,0 +1,42 @@ + + + + + ansible-test-image-rhel6 + my new node + + + + + + + ansible-custom-image-test-UAT + my new node + + + + + + + + + + rhel-ansible-full-test + RHEL Ansible Test + + + + + + + + rhel-ansible-full-test + RHEL Ansible Test + + + + + + + + diff --git a/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list_PAGINATED.xml b/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list_PAGINATED.xml new file mode 100644 index 0000000000..fdd5e01e12 --- /dev/null +++ b/libcloud/test/compute/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list_PAGINATED.xml @@ -0,0 +1,42 @@ + + + + + ansible-test-image-rhel6 + my new node + + + + + + + ansible-custom-image-test-UAT + my new node + + + + + + + + + + rhel-ansible-full-test + RHEL Ansible Test + + + + + + + + rhel-ansible-full-test + RHEL Ansible Test + + + + + + + + diff --git a/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_create.xml b/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_create.xml new file mode 100644 index 0000000000..993b85d6f3 --- /dev/null +++ b/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_create.xml @@ -0,0 +1,11 @@ + + + Create Anti Affinity Rule + SUCCESS + Request to create Server Anti-Affinity Rule between 'rhel-ansible-full-test' and 'rhel-ansible-full-test' on 'Deloitte Test' has been accepted and is being processed. + REASON_0 + + 5e10b1ab-68f2-4a8b-a49c-d88d623db665 + + + diff --git a/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_create_FAIL.xml b/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_create_FAIL.xml new file mode 100644 index 0000000000..eecc69d9c1 --- /dev/null +++ b/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_create_FAIL.xml @@ -0,0 +1,7 @@ + + + Create Anti Affinity Rule + ERROR + Server 'ansible-test-image-rhel6' (id 22f3544a-c874-4930-a31c-e9e513e51114) is already used in another Anti-Affinity Rule (id 07e3621a-a920-4a9a-943c-d8021f27f418). + REASON_692 + diff --git a/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_delete.xml b/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_delete.xml new file mode 100644 index 0000000000..0d73a1306c --- /dev/null +++ b/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_delete.xml @@ -0,0 +1,7 @@ + + + Delete Anti Affinity Rule + SUCCESS + Request to delete Server Anti-Affinity Rule between 'ansible-test-image-rhel6' and 'ansible-custom-image-test-UAT' on 'Deloitte Test' has been accepted and is being processed. + REASON_0 + diff --git a/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_delete_FAIL.xml b/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_delete_FAIL.xml new file mode 100644 index 0000000000..187f482aa7 --- /dev/null +++ b/libcloud/test/compute/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_delete_FAIL.xml @@ -0,0 +1,7 @@ + + + Delete Anti Affinity Rule + ERROR + Could not find Anti Affinity Rule with Id 07e3621a-a920-4a9a-943c-d8021f27f418 + REASON_693 + diff --git a/libcloud/test/compute/test_dimensiondata.py b/libcloud/test/compute/test_dimensiondata.py index 8ec9d2779a..cc8a0f8127 100644 --- a/libcloud/test/compute/test_dimensiondata.py +++ b/libcloud/test/compute/test_dimensiondata.py @@ -19,6 +19,7 @@ from xml.etree import ElementTree as ET import sys +from types import GeneratorType from libcloud.utils.py3 import httplib from libcloud.common.types import InvalidCredsError @@ -91,6 +92,13 @@ def test_list_nodes_response_PAGINATED(self): ret = self.driver.list_nodes() self.assertEqual(len(ret), 9) + def test_paginated_mcp2_call_with_page_size(self): + # cache org + self.driver.connection._get_orgId() + DimensionDataMockHttp.type = 'PAGESIZE50' + node_list_generator = self.driver.connection.paginated_request_with_orgId_api_2('server/server', page_size=50) + self.assertTrue(isinstance(node_list_generator, GeneratorType)) + # We're making sure here the filters make it to the URL # See _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_ALLFILTERS for asserts def test_list_nodes_response_strings_ALLFILTERS(self): @@ -665,6 +673,36 @@ def test_ex_create_firewall_rule_with_specific_source_ip(self): rule = self.driver.ex_create_firewall_rule(net, specific_source_ip_rule, 'FIRST') self.assertEqual(rule.id, 'd0a20f59-77b9-4f28-a63b-e58496b73a6c') + def test_ex_create_firewall_rule_ALL_VALUES(self): + net = self.driver.ex_get_network_domain('8cdfd607-f429-4df6-9352-162cfc0891be') + rules = self.driver.ex_list_firewall_rules(net) + for rule in rules: + self.driver.ex_create_firewall_rule(net, rule, 'LAST') + + def test_ex_create_firewall_rule_WITH_POSITION_RULE(self): + net = self.driver.ex_get_network_domain('8cdfd607-f429-4df6-9352-162cfc0891be') + rules = self.driver.ex_list_firewall_rules(net) + rule = self.driver.ex_create_firewall_rule(net, rules[-2], 'BEFORE', rules[-1]) + self.assertEqual(rule.id, 'd0a20f59-77b9-4f28-a63b-e58496b73a6c') + + def test_ex_create_firewall_rule_WITH_POSITION_RULE_STR(self): + net = self.driver.ex_get_network_domain('8cdfd607-f429-4df6-9352-162cfc0891be') + rules = self.driver.ex_list_firewall_rules(net) + rule = self.driver.ex_create_firewall_rule(net, rules[-2], 'BEFORE', 'RULE_WITH_SOURCE_AND_DEST') + self.assertEqual(rule.id, 'd0a20f59-77b9-4f28-a63b-e58496b73a6c') + + def test_ex_create_firewall_rule_FAIL_POSITION(self): + net = self.driver.ex_get_network_domain('8cdfd607-f429-4df6-9352-162cfc0891be') + rules = self.driver.ex_list_firewall_rules(net) + with self.assertRaises(ValueError): + self.driver.ex_create_firewall_rule(net, rules[0], 'BEFORE') + + def test_ex_create_firewall_rule_FAIL_POSITION_WITH_RULE(self): + net = self.driver.ex_get_network_domain('8cdfd607-f429-4df6-9352-162cfc0891be') + rules = self.driver.ex_list_firewall_rules(net) + with self.assertRaises(ValueError): + self.driver.ex_create_firewall_rule(net, rules[0], 'LAST', 'RULE_WITH_SOURCE_AND_DEST') + def test_ex_get_firewall_rule(self): net = self.driver.ex_get_network_domain('8cdfd607-f429-4df6-9352-162cfc0891be') rule = self.driver.ex_get_firewall_rule(net, 'd0a20f59-77b9-4f28-a63b-e58496b73a6c') @@ -790,6 +828,97 @@ def test_ex_get_image_by_id_customer_FAIL(self): with self.assertRaises(DimensionDataAPIException): self.driver.ex_get_base_image_by_id(image_id) + def test_ex_create_anti_affinity_rule(self): + node_list = self.driver.list_nodes() + success = self.driver.ex_create_anti_affinity_rule([node_list[0], node_list[1]]) + self.assertTrue(success) + + def test_ex_create_anti_affinity_rule_TUPLE(self): + node_list = self.driver.list_nodes() + success = self.driver.ex_create_anti_affinity_rule((node_list[0], node_list[1])) + self.assertTrue(success) + + def test_ex_create_anti_affinity_rule_TUPLE_STR(self): + node_list = self.driver.list_nodes() + success = self.driver.ex_create_anti_affinity_rule((node_list[0].id, node_list[1].id)) + self.assertTrue(success) + + def test_ex_create_anti_affinity_rule_FAIL_STR(self): + node_list = 'string' + with self.assertRaises(TypeError): + self.driver.ex_create_anti_affinity_rule(node_list) + + def test_ex_create_anti_affinity_rule_FAIL_EXISTING(self): + node_list = self.driver.list_nodes() + DimensionDataMockHttp.type = 'FAIL_EXISTING' + with self.assertRaises(DimensionDataAPIException): + self.driver.ex_create_anti_affinity_rule((node_list[0], node_list[1])) + + def test_ex_delete_anti_affinity_rule(self): + net_domain = self.driver.ex_list_network_domains()[0] + rule = self.driver.ex_list_anti_affinity_rules(network_domain=net_domain)[0] + success = self.driver.ex_delete_anti_affinity_rule(rule) + self.assertTrue(success) + + def test_ex_delete_anti_affinity_rule_STR(self): + net_domain = self.driver.ex_list_network_domains()[0] + rule = self.driver.ex_list_anti_affinity_rules(network_domain=net_domain)[0] + success = self.driver.ex_delete_anti_affinity_rule(rule.id) + self.assertTrue(success) + + def test_ex_delete_anti_affinity_rule_FAIL(self): + net_domain = self.driver.ex_list_network_domains()[0] + rule = self.driver.ex_list_anti_affinity_rules(network_domain=net_domain)[0] + DimensionDataMockHttp.type = 'FAIL' + with self.assertRaises(DimensionDataAPIException): + self.driver.ex_delete_anti_affinity_rule(rule) + + def test_ex_list_anti_affinity_rules_NETWORK_DOMAIN(self): + net_domain = self.driver.ex_list_network_domains()[0] + rules = self.driver.ex_list_anti_affinity_rules(network_domain=net_domain) + self.assertTrue(isinstance(rules, list)) + self.assertEqual(len(rules), 2) + self.assertTrue(isinstance(rules[0].id, str)) + self.assertTrue(isinstance(rules[0].node_list, list)) + + def test_ex_list_anti_affinity_rules_NETWORK(self): + network = self.driver.list_networks()[0] + rules = self.driver.ex_list_anti_affinity_rules(network=network) + self.assertTrue(isinstance(rules, list)) + self.assertEqual(len(rules), 2) + self.assertTrue(isinstance(rules[0].id, str)) + self.assertTrue(isinstance(rules[0].node_list, list)) + + def test_ex_list_anti_affinity_rules_NODE(self): + node = self.driver.list_nodes()[0] + rules = self.driver.ex_list_anti_affinity_rules(node=node) + self.assertTrue(isinstance(rules, list)) + self.assertEqual(len(rules), 2) + self.assertTrue(isinstance(rules[0].id, str)) + self.assertTrue(isinstance(rules[0].node_list, list)) + + def test_ex_list_anti_affinity_rules_PAGINATED(self): + net_domain = self.driver.ex_list_network_domains()[0] + DimensionDataMockHttp.type = 'PAGINATED' + rules = self.driver.ex_list_anti_affinity_rules(network_domain=net_domain) + self.assertTrue(isinstance(rules, list)) + self.assertEqual(len(rules), 4) + self.assertTrue(isinstance(rules[0].id, str)) + self.assertTrue(isinstance(rules[0].node_list, list)) + + def test_ex_list_anti_affinity_rules_ALLFILTERS(self): + net_domain = self.driver.ex_list_network_domains()[0] + DimensionDataMockHttp.type = 'ALLFILTERS' + rules = self.driver.ex_list_anti_affinity_rules(network_domain=net_domain, filter_id='FAKE_ID', filter_state='FAKE_STATE') + self.assertTrue(isinstance(rules, list)) + self.assertEqual(len(rules), 2) + self.assertTrue(isinstance(rules[0].id, str)) + self.assertTrue(isinstance(rules[0].node_list, list)) + + def test_ex_list_anti_affinity_rules_BAD_ARGS(self): + with self.assertRaises(ValueError): + self.driver.ex_list_anti_affinity_rules(network='fake_network', network_domain='fake_network_domain') + def test_priv_location_to_location_id(self): location = self.driver.ex_get_location_by_id('NA9') self.assertEqual( @@ -979,6 +1108,30 @@ def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_POST.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule(self, method, url, body, headers): + body = self.fixtures.load( + 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_create.xml' + ) + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_FAIL_EXISTING(self, method, url, body, headers): + body = self.fixtures.load( + 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_create_FAIL.xml' + ) + return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK]) + + def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_07e3621a_a920_4a9a_943c_d8021f27f418(self, method, url, body, headers): + body = self.fixtures.load( + 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_delete.xml' + ) + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_07e3621a_a920_4a9a_943c_d8021f27f418_FAIL(self, method, url, body, headers): + body = self.fixtures.load( + 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_antiAffinityRule_delete_FAIL.xml' + ) + return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK]) + def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server(self, method, url, body, headers): body = self.fixtures.load( 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server.xml') @@ -1026,8 +1179,15 @@ def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server(self, method, u 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_PAGESIZE50(self, method, url, body, headers): + if not url.endswith('pageSize=50'): + raise ValueError("pageSize is not set as expected") + body = self.fixtures.load( + 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_PAGINATED(self, method, url, body, headers): - if url.endswith('pageNumber=2'): + if 'pageNumber=2' in url: body = self.fixtures.load( 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) @@ -1069,6 +1229,42 @@ def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_ALLFILTERS(self 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule(self, method, url, body, headers): + body = self.fixtures.load( + 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list.xml' + ) + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_ALLFILTERS(self, method, url, body, headers): + (_, params) = url.split('?') + parameters = params.split('&') + for parameter in parameters: + (key, value) = parameter.split('=') + if key == 'id': + assert value == 'FAKE_ID' + elif key == 'state': + assert value == 'FAKE_STATE' + elif key == 'pageSize': + assert value == '250' + elif key == 'networkDomainId': + pass + else: + raise ValueError("Could not find in url parameters {0}:{1}".format(key, value)) + body = self.fixtures.load( + 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list.xml' + ) + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_PAGINATED(self, method, url, body, headers): + if 'pageNumber=2' in url: + body = self.fixtures.load( + 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + else: + body = self.fixtures.load( + 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_antiAffinityRule_list_PAGINATED.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_infrastructure_datacenter(self, method, url, body, headers): if url.endswith('id=NA9'): body = self.fixtures.load(