Repository: libcloud Updated Branches: refs/heads/trunk 69f84e61a -> d608085e3
Cloudstack - Added VPC support and Egress Firewall rule support Signed-off-by: Sebastien Goasguen <[email protected]> This closes: #364 Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/d608085e Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/d608085e Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/d608085e Branch: refs/heads/trunk Commit: d608085e3da79240b2b56709ef6adb58bd9c9dab Parents: 69f84e6 Author: Jeroen de Korte <[email protected]> Authored: Thu Sep 25 15:27:20 2014 +0200 Committer: Sebastien Goasguen <[email protected]> Committed: Fri Sep 26 04:41:32 2014 -0400 ---------------------------------------------------------------------- CHANGES.rst | 4 + libcloud/compute/drivers/cloudstack.py | 350 ++++++++++++++++++- .../createEgressFirewallRule_default.json | 1 + .../fixtures/cloudstack/createVPC_default.json | 1 + .../deleteEgressFirewallRule_default.json | 1 + .../fixtures/cloudstack/deleteVPC_default.json | 1 + .../listEgressFirewallRules_default.json | 1 + .../cloudstack/listVPCOfferings_default.json | 1 + .../fixtures/cloudstack/listVPCs_default.json | 1 + .../queryAsyncJobResult_deleteVPC.json | 1 + libcloud/test/compute/test_cloudstack.py | 90 +++++ 11 files changed, 451 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/CHANGES.rst ---------------------------------------------------------------------- diff --git a/CHANGES.rst b/CHANGES.rst index 372d271..fba5f48 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -122,6 +122,10 @@ Compute (GITHUB-363, LIBCLOUD-616) [Oleg Suharev] +- Added VPC support and Egress Firewall rule support fo CloudStack + (GITHUB-363) + [Jeroen de Korte] + Storage ~~~~~~~ http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/compute/drivers/cloudstack.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/cloudstack.py b/libcloud/compute/drivers/cloudstack.py index 858553d..0129e07 100644 --- a/libcloud/compute/drivers/cloudstack.py +++ b/libcloud/compute/drivers/cloudstack.py @@ -217,6 +217,40 @@ RESOURCE_EXTRA_ATTRIBUTES_MAP = { 'transform_func': str } }, + 'vpc': { + 'created': { + 'key_name': 'created', + 'transform_func': str + }, + 'domain': { + 'key_name': 'domain', + 'transform_func': str + }, + 'domain_id': { + 'key_name': 'domainid', + 'transform_func': int + }, + 'network_domain': { + 'key_name': 'networkdomain', + 'transform_func': str + }, + 'state': { + 'key_name': 'state', + 'transform_func': str + }, + 'vpc_offering_id': { + 'key_name': 'vpcofferingid', + 'transform_func': str + }, + 'zone_name': { + 'key_name': 'zonename', + 'transform_func': str + }, + 'zone_id': { + 'key_name': 'zoneid', + 'transform_func': str + } + }, 'project': { 'account': {'key_name': 'account', 'transform_func': str}, 'cpuavailable': {'key_name': 'cpuavailable', 'transform_func': int}, @@ -426,6 +460,62 @@ class CloudStackFirewallRule(object): return self.__class__ is other.__class__ and self.id == other.id +class CloudStackEgressFirewallRule(object): + """ + A egress firewall rule. + """ + + def __init__(self, id, network_id, cidr_list, protocol, + icmp_code=None, icmp_type=None, + start_port=None, end_port=None): + + """ + A egress firewall rule. + + @note: This is a non-standard extension API, and only works for + CloudStack. + + :param id: Firewall Rule ID + :type id: ``int`` + + :param network_id: the id network network for the egress firwall + services + :type network_id: ``str`` + + :param protocol: TCP/IP Protocol (TCP, UDP) + :type protocol: ``str`` + + :param cidr_list: cidr list + :type cidr_list: ``str`` + + :param icmp_code: Error code for this icmp message + :type icmp_code: ``int`` + + :param icmp_type: Type of the icmp message being sent + :type icmp_type: ``int`` + + :param start_port: start of port range + :type start_port: ``int`` + + :param end_port: end of port range + :type end_port: ``int`` + + :rtype: :class:`CloudStackEgressFirewallRule` + """ + + self.id = id + self.network_id = network_id + self.cidr_list = cidr_list + self.protocol = protocol + self.icmp_code = icmp_code + self.icmp_type = icmp_type + self.start_port = start_port + self.end_port = end_port + + def __eq__(self, other): + return self.__class__ is other.__class__ and self.id == other.id + + class CloudStackIPForwardingRule(object): """ A NAT/firewall forwarding rule. @@ -564,6 +654,51 @@ class CloudStackNetworkOffering(object): self.driver.name)) +class CloudStackVPC(object): + """ + Class representing a CloudStack VPC. + """ + + def __init__(self, display_text, name, vpc_offering_id, id, zone_id, cidr, + driver, extra=None): + self.display_text = display_text + self.name = name + self.vpc_offering_id = vpc_offering_id + self.id = id + self.zone_id = zone_id + self.cidr = cidr + self.driver = driver + self.extra = extra or {} + + def __repr__(self): + return (('<CloudStackVPC: id=%s, displaytext=%s, name=%s, ' + 'vpc_offering_id=%s, zoneid=%s, cidr=%s, driver%s>') + % (self.id, self.displaytext, self.name, + self.vpc_offering_id, self.zoneid, self.cidr, + self.driver.name)) + + +class CloudStackVPCOffering(object): + """ + Class representing a CloudStack VPC Offering. + """ + + def __init__(self, name, display_text, id, + driver, extra=None): + self.name = name + self.display_text = display_text + self.id = id + self.driver = driver + self.extra = extra or {} + + def __repr__(self): + return (('<CloudStackVPCOffering: id=%s, name=%s, ' + 'display_text=%s, ' + 'driver%s>') + % (self.id, self.name, self.display_text, + self.driver.name)) + + class CloudStackProject(object): """ Class representing a CloudStack Project. @@ -1132,6 +1267,125 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): method='GET') return True + def ex_list_vpc_offerings(self): + """ + List the available vpc offerings + + :rtype ``list`` of :class:`CloudStackVPCOffering` + """ + res = self._sync_request(command='listVPCOfferings', + method='GET') + vpcoffers = res.get('vpcoffering', []) + + vpcofferings = [] + + for vpcoffer in vpcoffers: + vpcofferings.append(CloudStackVPCOffering( + vpcoffer['name'], + vpcoffer['displaytext'], + vpcoffer['id'], + self)) + + return vpcofferings + + def ex_list_vpcs(self): + """ + List the available VPCs + + :rtype ``list`` of :class:`CloudStackVPC` + """ + + res = self._sync_request(command='listVPCs', + method='GET') + vpcs = res.get('vpc', []) + + networks = [] + for vpc in vpcs: + + networks.append(CloudStackVPC( + vpc['displaytext'], + vpc['name'], + vpc['vpcofferingid'], + vpc['id'], + vpc['zoneid'], + vpc['cidr'], + self)) + + return networks + + def ex_create_vpc(self, cidr, display_text, name, vpc_offering, + zoneid): + """ + + Creates a VPC, only available in advanced zones. + + :param cidr: the cidr of the VPC. All VPC guest networks' cidrs + should be within this CIDR + + :type display_text: ``str`` + + :param display_text: the display text of the VPC + :type display_text: ``str`` + + :param name: the name of the VPC + :type name: ``str`` + + :param vpc_offering: the ID of the VPC offering + :type vpc_offering: :class:'CloudStackVPCOffering` + + :param zoneid: the ID of the availability zone + :type zoneid: ``str`` + + :rtype: :class:`CloudStackVPC` + + """ + + extra_map = RESOURCE_EXTRA_ATTRIBUTES_MAP['vpc'] + + args = { + 'cidr': cidr, + 'displaytext': display_text, + 'name': name, + 'vpcofferingid': vpc_offering.id, + 'zoneid': zoneid, + } + + result = self._sync_request(command='createVPC', + params=args, + method='GET') + + extra = self._get_extra_dict(result, extra_map) + + vpc = CloudStackVPC(display_text, + name, + vpc_offering.id, + result['id'], + zoneid, + cidr, + self, + extra=extra) + + return vpc + + def ex_delete_vpc(self, vpc): + """ + + Deletes a VPC, only available in advanced zones. + + :param vpc: The VPC + :type vpc: :class: 'CloudStackVPC' + + :rtype: ``bool`` + + """ + + args = {'id': vpc.id} + + self._async_request(command='deleteVPC', + params=args, + method='GET') + return True + def ex_list_projects(self): """ List the available projects @@ -1431,7 +1685,8 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): ips.append(CloudStackAddress(ip['id'], ip['ipaddress'], self, - ip['associatednetworkid'])) + ip.get('associatednetworkid', []))) + return ips def ex_allocate_public_ip(self, location=None): @@ -1562,6 +1817,99 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): method='GET') return res['success'] + def ex_list_egress_firewall_rules(self): + """ + Lists all agress Firewall Rules + + :rtype: ``list`` of :class:`CloudStackEgressFirewallRule` + """ + rules = [] + result = self._sync_request(command='listEgressFirewallRules', + method='GET') + for rule in result['firewallrule']: + rules.append(CloudStackEgressFirewallRule(rule['id'], + rule['networkid'], + rule['cidrlist'], + rule['protocol'], + rule.get('icmpcode'), + rule.get('icmptype'), + rule.get('startport'), + rule.get('endport'))) + + return rules + + def ex_create_egress_firewall_rule(self, network_id, cidr_list, protocol, + icmp_code=None, icmp_type=None, + start_port=None, end_port=None): + """ + Creates a Firewalle Rule + + :param network_id: the id network network for the egress firwall + services + :type network_id: ``str`` + + :param cidr_list: cidr list + :type cidr_list: ``str`` + + :param protocol: TCP/IP Protocol (TCP, UDP) + :type protocol: ``str`` + + :param icmp_code: Error code for this icmp message + :type icmp_code: ``int`` + + :param icmp_type: Type of the icmp message being sent + :type icmp_type: ``int`` + + :param start_port: start of port range + :type start_port: ``int`` + + :param end_port: end of port range + :type end_port: ``int`` + + :rtype: :class:`CloudStackEgressFirewallRule` + """ + args = { + 'networkid': network_id, + 'cidrlist': cidr_list, + 'protocol': protocol + } + if icmp_code is not None: + args['icmpcode'] = int(icmp_code) + if icmp_type is not None: + args['icmptype'] = int(icmp_type) + if start_port is not None: + args['startport'] = int(start_port) + if end_port is not None: + args['endport'] = int(end_port) + + result = self._async_request(command='createEgressFirewallRule', + params=args, + method='GET') + + rule = CloudStackEgressFirewallRule(result['firewallrule']['id'], + network_id, + cidr_list, + protocol, + icmp_code, + icmp_type, + start_port, + end_port) + return rule + + def ex_delete_egress_firewall_rule(self, firewall_rule): + """ + Remove a Firewall rule. + + :param egress_firewall_rule: Firewall rule which should be used + :type egress_firewall_rule: :class:`CloudStackEgressFirewallRule` + + :rtype: ``bool`` + """ + res = self._async_request(command='deleteEgressFirewallRule', + params={'id': firewall_rule.id}, + method='GET') + return res['success'] + def ex_list_port_forwarding_rules(self): """ Lists all Port Forwarding Rules http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/test/compute/fixtures/cloudstack/createEgressFirewallRule_default.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/createEgressFirewallRule_default.json b/libcloud/test/compute/fixtures/cloudstack/createEgressFirewallRule_default.json new file mode 100644 index 0000000..104b80a --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/createEgressFirewallRule_default.json @@ -0,0 +1 @@ +{ "createegressfirewallruleresponse" : {"jobid":1149341,"id":172465} } http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/test/compute/fixtures/cloudstack/createVPC_default.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/createVPC_default.json b/libcloud/test/compute/fixtures/cloudstack/createVPC_default.json new file mode 100644 index 0000000..e1c2bed --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/createVPC_default.json @@ -0,0 +1 @@ +{ "createvpcresponse" : {"id":"c78499e1-b3a2-4a2a-9759-c2bcb1b79cd4","jobid":"f618f672-c714-4031-8c79-bb1300a2163c"} } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/test/compute/fixtures/cloudstack/deleteEgressFirewallRule_default.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/deleteEgressFirewallRule_default.json b/libcloud/test/compute/fixtures/cloudstack/deleteEgressFirewallRule_default.json new file mode 100644 index 0000000..3283e55 --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/deleteEgressFirewallRule_default.json @@ -0,0 +1 @@ +{ "deleteegressfirewallruleresponse" : {"jobid":1149342} } http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/test/compute/fixtures/cloudstack/deleteVPC_default.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/deleteVPC_default.json b/libcloud/test/compute/fixtures/cloudstack/deleteVPC_default.json new file mode 100644 index 0000000..300c113 --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/deleteVPC_default.json @@ -0,0 +1 @@ +{ "deletevpcresponse" : {"jobid":"deleteVPC"} } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/test/compute/fixtures/cloudstack/listEgressFirewallRules_default.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/listEgressFirewallRules_default.json b/libcloud/test/compute/fixtures/cloudstack/listEgressFirewallRules_default.json new file mode 100644 index 0000000..d51bba7 --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/listEgressFirewallRules_default.json @@ -0,0 +1 @@ +{ "listegressfirewallrulesresponse" : { "count":1 ,"firewallrule" : [ {"id":"7d4e2924-49b6-4a5a-9a98-69f2f0f73c69","protocol":"tcp","startport":"80","endport":"80","networkid":"874be2ca-20a7-4360-80e9-7356c0018c0b","state":"Active","cidrlist":"192.168.0.0/16","tags":[]} ] } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/test/compute/fixtures/cloudstack/listVPCOfferings_default.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/listVPCOfferings_default.json b/libcloud/test/compute/fixtures/cloudstack/listVPCOfferings_default.json new file mode 100644 index 0000000..368a143 --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/listVPCOfferings_default.json @@ -0,0 +1 @@ +{ "listvpcofferingsresponse" : { "count":1 ,"vpcoffering" : [ {"id":"cd7dbd68-4333-4507-b80d-9840cab32841","name":"Default VPC offering with Netscaler","displaytext":"Default VPC offering with Netscaler","isdefault":false,"state":"Enabled","service":[{"name":"Vpn","provider":[{"name":"VpcVirtualRouter"}]},{"name":"NetworkACL","provider":[{"name":"VpcVirtualRouter"}]},{"name":"PortForwarding","provider":[{"name":"VpcVirtualRouter"}]},{"name":"Dns","provider":[{"name":"VpcVirtualRouter"}]},{"name":"UserData","provider":[{"name":"VpcVirtualRouter"}]},{"name":"Lb","provider":[{"name":"Netscaler"},{"name":"InternalLbVm"}]},{"name":"SourceNat","provider":[{"name":"VpcVirtualRouter"}]},{"name":"StaticNat","provider":[{"name":"VpcVirtualRouter"}]},{"name":"Dhcp","provider":[{"name":"VpcVirtualRouter"}]}]} ] } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/test/compute/fixtures/cloudstack/listVPCs_default.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/listVPCs_default.json b/libcloud/test/compute/fixtures/cloudstack/listVPCs_default.json new file mode 100644 index 0000000..d349e18 --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/listVPCs_default.json @@ -0,0 +1 @@ +{ "listvpcsresponse" : { "count":1 ,"vpc" : [ {"id":"6adc8ad1-a8a1-4a4e-af75-2c3643297041","name":"test","displaytext":"test","state":"Enabled","zoneid":"2","zonename":"TEST-DC-1","service":[{"name":"Vpn","provider":[{"name":"VpcVirtualRouter"}]},{"name":"Connectivity","provider":[{"name":"NiciraNvp"}]},{"name":"NetworkACL","provider":[{"name":"VpcVirtualRouter"}]},{"name":"PortForwarding","provider":[{"name":"VpcVirtualRouter"}]},{"name":"Dns","provider":[{"name":"VpcVirtualRouter"}]},{"name":"UserData","provider":[{"name":"VpcVirtualRouter"}]},{"name":"Lb","provider":[{"name":"VpcVirtualRouter"},{"name":"InternalLbVm"}]},{"name":"SourceNat","provider":[{"name":"VpcVirtualRouter"}]},{"name":"StaticNat","provider":[{"name":"VpcVirtualRouter"}]},{"name":"Dhcp","provider":[{"name":"VpcVirtualRouter"}]}],"cidr":"10.1.1.0/16","vpcofferingid":"ef58092b-8547-4d41-8dc3-cdaa471e12b1","account":"test_admin","domainid":"ee90435a-bba8-427f-90f9-02f9bf9e03aa","domain":"test","network":[],"rest artrequired":false,"networkdomain":"test.local","tags":[]} ] } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/test/compute/fixtures/cloudstack/queryAsyncJobResult_deleteVPC.json ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/cloudstack/queryAsyncJobResult_deleteVPC.json b/libcloud/test/compute/fixtures/cloudstack/queryAsyncJobResult_deleteVPC.json new file mode 100644 index 0000000..b4a3041 --- /dev/null +++ b/libcloud/test/compute/fixtures/cloudstack/queryAsyncJobResult_deleteVPC.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"accountid":"5d7d4c5e-7e0b-4ac2-8550-713180d8a342","userid":"5fb4b286-ac58-44a7-acae-d47dbbac78d1","cmd":"org.apache.cloudstack.api.command.user.vpc.DeleteVPCCmd","jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"success":true},"created":"2014-09-25T00:11:31+0200","jobid":"cfa5c4f5-d312-4b18-9197-385ef169726e"} } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/d608085e/libcloud/test/compute/test_cloudstack.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/test_cloudstack.py b/libcloud/test/compute/test_cloudstack.py index 00ba306..cd72b2b 100644 --- a/libcloud/test/compute/test_cloudstack.py +++ b/libcloud/test/compute/test_cloudstack.py @@ -295,6 +295,58 @@ class CloudStackCommonTestCase(TestCaseMixin): result = self.driver.ex_delete_network(network=network) self.assertTrue(result) + def test_ex_list_vpc_offerings(self): + _, fixture = CloudStackMockHttp()._load_fixture( + 'listVPCOfferings_default.json') + fixture_vpcoffers = \ + fixture['listvpcofferingsresponse']['vpcoffering'] + + vpcoffers = self.driver.ex_list_vpc_offerings() + + for i, vpcoffer in enumerate(vpcoffers): + self.assertEqual(vpcoffer.id, fixture_vpcoffers[i]['id']) + self.assertEqual(vpcoffer.name, + fixture_vpcoffers[i]['name']) + self.assertEqual(vpcoffer.display_text, + fixture_vpcoffers[i]['displaytext']) + + def test_ex_list_vpcs(self): + _, fixture = CloudStackMockHttp()._load_fixture( + 'listVPCs_default.json') + fixture_vpcs = fixture['listvpcsresponse']['vpc'] + + vpcs = self.driver.ex_list_vpcs() + + for i, vpc in enumerate(vpcs): + self.assertEqual(vpc.id, fixture_vpcs[i]['id']) + self.assertEqual(vpc.display_text, fixture_vpcs[i]['displaytext']) + self.assertEqual(vpc.name, fixture_vpcs[i]['name']) + self.assertEqual(vpc.vpc_offering_id, + fixture_vpcs[i]['vpcofferingid']) + self.assertEqual(vpc.zone_id, fixture_vpcs[i]['zoneid']) + + def test_ex_create_vpc(self): + _, fixture = CloudStackMockHttp()._load_fixture( + 'createVPC_default.json') + + fixture_vpc = fixture['createvpcresponse'] + + vpcoffer = self.driver.ex_list_vpc_offerings()[0] + vpc = self.driver.ex_create_vpc(cidr='10.1.1.0/16', + display_text='cloud.local', + name='cloud.local', + vpc_offering=vpcoffer, + zoneid="2") + + self.assertEqual(vpc.id, fixture_vpc['id']) + + def test_ex_delete_vpc(self): + + vpc = self.driver.ex_list_vpcs()[0] + + result = self.driver.ex_delete_vpc(vpc=vpc) + self.assertTrue(result) + def test_ex_list_projects(self): _, fixture = CloudStackMockHttp()._load_fixture( 'listProjects_default.json') @@ -634,6 +686,44 @@ class CloudStackCommonTestCase(TestCaseMixin): self.assertIsNone(rule.start_port) self.assertIsNone(rule.end_port) + def test_ex_list_egress_firewall_rules(self): + rules = self.driver.ex_list_egress_firewall_rules() + self.assertEqual(len(rules), 1) + rule = rules[0] + self.assertEqual(rule.network_id, '874be2ca-20a7-4360-80e9-7356c0018c0b') + self.assertEqual(rule.cidr_list, '192.168.0.0/16') + self.assertEqual(rule.protocol, 'tcp') + self.assertIsNone(rule.icmp_code) + self.assertIsNone(rule.icmp_type) + self.assertEqual(rule.start_port, '80') + self.assertEqual(rule.end_port, '80') + + def test_ex_delete_egress_firewall_rule(self): + rules = self.driver.ex_list_egress_firewall_rules() + res = self.driver.ex_delete_egress_firewall_rule(rules[0]) + self.assertTrue(res) + + def test_ex_create_egress_firewall_rule(self): + network_id = '874be2ca-20a7-4360-80e9-7356c0018c0b' + cidr_list = '192.168.0.0/16' + protocol = 'TCP' + start_port = 33 + end_port = 34 + rule = self.driver.ex_create_egress_firewall_rule( + network_id, + cidr_list, + protocol, + start_port=start_port, + end_port=end_port) + + self.assertEqual(rule.network_id, network_id) + self.assertEqual(rule.cidr_list, cidr_list) + self.assertEqual(rule.protocol, protocol) + self.assertIsNone(rule.icmp_code) + self.assertIsNone(rule.icmp_type) + self.assertEqual(rule.start_port, start_port) + self.assertEqual(rule.end_port, end_port) + def test_ex_list_port_forwarding_rules(self): rules = self.driver.ex_list_port_forwarding_rules() self.assertEqual(len(rules), 1)
