From d98580f105dba2c720adb46c962f574a1fad0335 Mon Sep 17 00:00:00 2001 From: ysssasaki <64194789+ysssasaki@users.noreply.github.com> Date: Thu, 17 Mar 2022 13:42:23 +0900 Subject: [PATCH] Support security group (#101) * Revert "Revert ":sparkles: IF-6816 add security groups api (#89)" (#96)" This reverts commit a77f2df99e25061e6e95eafa813569c8ed7ec0a0. * Revert "Revert ":sparkles: IF-7067 add security group rules api (#90)" (#95)" This reverts commit 1edbfbc2f4139684d887ebc947c61cb35f645ae0. * Revert "Revert ":bug: IF-7068 fix existing api (#91)" (#94)" This reverts commit c5b99e18c5adb34f46194ef4ffec5dff1efb90ae. * :sparkles: IF-7192 security groups api query support (#93) - List Security Group API - List Security Group Rule API Co-authored-by: a-oi-xon Co-authored-by: a-oi-xon <91597807+a-oi-xon@users.noreply.github.com> --- ecl/network/v2/_proxy.py | 189 +++++++++++++++++- ecl/network/v2/port.py | 3 + ecl/network/v2/quota.py | 2 + ecl/network/v2/security_group.py | 39 ++++ ecl/network/v2/security_group_rule.py | 52 +++++ ecl/tests/unit/network/v2/test_port.py | 12 +- ecl/tests/unit/network/v2/test_quota.py | 30 +-- .../unit/network/v2/test_security_group.py | 59 ++++++ .../network/v2/test_security_group_rule.py | 59 ++++++ 9 files changed, 426 insertions(+), 19 deletions(-) create mode 100644 ecl/network/v2/security_group.py create mode 100644 ecl/network/v2/security_group_rule.py create mode 100644 ecl/tests/unit/network/v2/test_security_group.py create mode 100644 ecl/tests/unit/network/v2/test_security_group_rule.py diff --git a/ecl/network/v2/_proxy.py b/ecl/network/v2/_proxy.py index fa1d988..211cbfa 100755 --- a/ecl/network/v2/_proxy.py +++ b/ecl/network/v2/_proxy.py @@ -13,6 +13,8 @@ from ecl.network.v2 import physical_port as _physical_port from ecl.network.v2 import quota as _quota from ecl.network.v2 import reserved_address as _reserved_address +from ecl.network.v2 import security_group as _security_group +from ecl.network.v2 import security_group_rule as _security_group_rule from ecl.network.v2 import firewall as _firewall from ecl.network.v2 import firewall_interface as _firewall_if from ecl.network.v2 import firewall_plan as _firewall_plan @@ -186,7 +188,7 @@ def get_extension(self, extension): def create_port(self, admin_state_up=None, allowed_address_pairs=None, mac_address=None, description=None, device_id=None, device_owner=None, fixed_ips=None, name=None, - network_id=None, segmentation_id=None, + network_id=None, security_groups=None, segmentation_id=None, segmentation_type=None, tags=None): """Create a new port from attributes @@ -201,6 +203,7 @@ def create_port(self, admin_state_up=None, allowed_address_pairs=None, e.g. [{"ip_address": , "subnet_id": }, ] :param string name: The name of port to create :param string network_id: The network id of port to create + :param array security_groups: The security groups ids of port to create :param int segmentation_id: The segmentation id of port to create :param string segmentation_type: The segmentation type of port to create :param dict tags: tags of port @@ -227,6 +230,8 @@ def create_port(self, admin_state_up=None, allowed_address_pairs=None, body["name"] = name if network_id: body["network_id"] = network_id + if security_groups: + body["security_groups"] = security_groups if segmentation_id: body["segmentation_id"] = segmentation_id if segmentation_type: @@ -303,6 +308,7 @@ def update_port(self, port, **params): * array fixed_ips: The fixed ips of port to update e.g. [{"ip_address": , "subnet_id": }, ] * string name: The name of port to update + * array security_groups: The security groups ids of port to update * int segmentation_id: The segmentation id of port to update * string segmentation_type: The segmentation type of port to update * dict tags: tags of port @@ -496,6 +502,187 @@ def get_reserved_address(self, reserved_address): """ return self._get(_reserved_address.ReservedAddress, reserved_address) + def security_groups(self, **query): + """ List all visible security-groups. + + :param query: Query parameters to select results + :return: A list of security-group objects + :rtype: :class:`~ecl.network.v2.security_group.SecurityGroup` + """ + return list(self._list(_security_group.SecurityGroup, + paginated=False, **query)) + + def create_security_group(self, description=None, name=None, tags=None, + tenant_id=None): + """Create security-group. + + :param string description: Security group description. + :param string name: Security group name. + :param dict tags: Security Group tags. + :param string tenant_id: The owner name of security group. + :returns: The results of security-group creation + :rtype: :class:`~ecl.network.v2.security_group.SecurityGroup` + """ + body = dict() + if description: + body["description"] = description + if name: + body["name"] = name + if tags: + body["tags"] = tags + if tenant_id: + body["tenant_id"] = tenant_id + + return self._create(_security_group.SecurityGroup, **body) + + def get_security_group(self, security_group): + """Show details for security-group. + + :param security_group: The value can be the ID of a security-group or + a :class:`~ecl.network.v2.security_group.SecurityGroup` instance. + :returns: One :class:`~ecl.network.v2.security_group.SecurityGroup` + :raises: :class:`~ecl.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_security_group.SecurityGroup, security_group) + + def update_security_group(self, security_group, **params): + """Update security-group. + + :param security_group: Either the id of a security-group or + a :class:`~ecl.network.v2.security_group.SecurityGroup` instance. + :param kwargs params: Parameters for security-group update. + + * string description: Security group description. + * string name: Security group name. + * dict tags: Security Group tags. + + :returns: The updated security-group + :rtype: :class:`~ecl.network.v2.security_group.SecurityGroup` + """ + if not isinstance(security_group, _security_group.SecurityGroup): + # security_group is the ID + security_group = self._get_resource(_security_group.SecurityGroup, + security_group) + security_group._body.clean() + + return self._update(_security_group.SecurityGroup, + security_group, **params) + + def delete_security_group(self, security_group, ignore_missing=False): + """Delete security-group. + + :param security_group: The value can be either the ID of + a security-group or + a :class:`~ecl.network.v2.security_group.SecurityGroup` instance. + :param bool ignore_missing: When set to ``False`` :class: + `~ecl.exceptions.ResourceNotFound` will + be raised when the security-group does + not exist. When set to ``True``, + no exception will be set when attempting + to delete a nonexistent security-group. + :returns: ``None`` + """ + self._delete(_security_group.SecurityGroup, security_group, + ignore_missing=ignore_missing) + + def security_group_rules(self, **query): + """ List all visible security-group-rules. + + :param query: Query parameters to select results + :return: A list of security-group-rule objects + :rtype: :class:`~ecl.network.v2.security_group_rule.SecurityGroupRule` + """ + return list(self._list(_security_group_rule.SecurityGroupRule, + paginated=False, **query)) + + def create_security_group_rule(self, security_group_id, direction, + description=None, ethertype=None, + port_range_max=None, port_range_min=None, + protocol=None, remote_group_id=None, + remote_ip_prefix=None, tenant_id=None): + """Create security-group-rule. + + :param string security_group_id: Security group id. + :param string direction: Direction in which the security group rule + is applied. + :param string description: Security group rule description. + :param string ethertype: Addresses represented in CIDR must match + the ingress or egress rules. + :param int port_range_max: The maximum port number in the range that + is matched by the security group rule. + :param int port_range_min: The minimum port number in the range that + is matched by the security group rule. + :param string protocol: Protocol name or number in string format. + e.g. "ICMP" or "1" + :param string remote_group_id: The remote group UUID to associate + with this security group rule. Only + either one of remote_group_id and + remote_ip_prefix have to be specified. + :param string remote_ip_prefix: The IP address prefix to associate + with this security group rule. Only + either one of remote_group_id and + remote_ip_prefix have to be specified. + :param string tenant_id: The owner name of security group rule. + :returns: The results of security-group-rule creation + :rtype: :class:`~ecl.network.v2.security_group_rule.SecurityGroupRule` + """ + body = { + "security_group_id": security_group_id, + "direction": direction + } + if description: + body["description"] = description + if ethertype: + body["ethertype"] = ethertype + if port_range_max is not None: + body["port_range_max"] = port_range_max + if port_range_min is not None: + body["port_range_min"] = port_range_min + if protocol: + body["protocol"] = protocol + if remote_group_id: + body["remote_group_id"] = remote_group_id + if remote_ip_prefix: + body["remote_ip_prefix"] = remote_ip_prefix + if tenant_id: + body["tenant_id"] = tenant_id + + return self._create(_security_group_rule.SecurityGroupRule, **body) + + def get_security_group_rule(self, security_group_rule): + """Show details for security-group-rule. + + :param security_group_rule: The value can be the ID of + a security-group-rule or + a :class:`~ecl.network.v2.security_group_rule.SecurityGroupRule` + instance. + :returns: :class:`~ecl.network.v2.security_group_rule.SecurityGroupRule` + :raises: :class:`~ecl.exceptions.ResourceNotFound` + when no resource can be found. + """ + return self._get(_security_group_rule.SecurityGroupRule, + security_group_rule) + + def delete_security_group_rule(self, security_group_rule, + ignore_missing=False): + """Delete security-group-rule. + + :param security_group_rule: The value can be either the ID of + a security-group-rule or + a :class:`~ecl.network.v2.security_group_rule.SecurityGroupRule` + instance. + :param bool ignore_missing: When set to ``False`` :class: + `~ecl.exceptions.ResourceNotFound` will + be raised when the security-group-rule does + not exist. When set to ``True``, + no exception will be set when attempting + to delete a nonexistent security-group-rule. + :returns: ``None`` + """ + self._delete(_security_group_rule.SecurityGroupRule, + security_group_rule, ignore_missing=ignore_missing) + def firewalls(self, **query): """ List all visible firewalls. diff --git a/ecl/network/v2/port.py b/ecl/network/v2/port.py index 9963a65..66b49f4 100755 --- a/ecl/network/v2/port.py +++ b/ecl/network/v2/port.py @@ -73,6 +73,9 @@ class Port(base.NetworkBaseResource): #: users can specify a project ID other than their own. project_id = resource2.Body('tenant_id') + #: The IDs of security groups applied to the port. + security_groups = resource2.Body('security_groups') + #: The segmentation ID of ports. segmentation_id = resource2.Body('segmentation_id', type=int) diff --git a/ecl/network/v2/quota.py b/ecl/network/v2/quota.py index 7f87fc1..0b4ba7b 100755 --- a/ecl/network/v2/quota.py +++ b/ecl/network/v2/quota.py @@ -45,6 +45,8 @@ class Quota(resource2.Resource): vpn_gateway = resource2.Body('vpn_gateway', type=int) #: The maximum amount of public ip you can create. *Type: int* public_ip = resource2.Body('public_ip', type=int) + #: The maximum amount of security group you can create. *Type: int* + security_group = resource2.Body('security_group', type=int) class QuotaDefault(Quota): diff --git a/ecl/network/v2/security_group.py b/ecl/network/v2/security_group.py new file mode 100644 index 0000000..f371429 --- /dev/null +++ b/ecl/network/v2/security_group.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from ecl.network import network_service +from ecl import resource2 + + +class SecurityGroup(resource2.Resource): + """SecurityGroup Resource""" + resource_key = 'security_group' + resources_key = 'security_groups' + service = network_service.NetworkService("v2.0") + base_path = '/' + service.version + '/security-groups' + + # query parameter names + _query_mapping = resource2.QueryParameters( + 'description', 'id', 'name', 'status', 'tenant_id') + + # capabilities + allow_list = True + allow_create = True + allow_get = True + allow_update = True + allow_delete = True + + # Properties + # Security group description. + description = resource2.Body('description') + # Security group unique id. + id = resource2.Body('id') + # Security group name. + name = resource2.Body('name') + # Security group status. + status = resource2.Body('status') + # Security Group tags. + tags = resource2.Body('tags') + # The owner name of security group. + tenant_id = resource2.Body('tenant_id') + # Security group rules + security_group_rules = resource2.Body('security_group_rules', type=list) diff --git a/ecl/network/v2/security_group_rule.py b/ecl/network/v2/security_group_rule.py new file mode 100644 index 0000000..541e9fb --- /dev/null +++ b/ecl/network/v2/security_group_rule.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +from ecl.network import network_service +from ecl import resource2 + + +class SecurityGroupRule(resource2.Resource): + """SecurityGroupRule Resource""" + resource_key = 'security_group_rule' + resources_key = 'security_group_rules' + service = network_service.NetworkService("v2.0") + base_path = '/' + service.version + '/security-group-rules' + + # query parameter names + _query_mapping = resource2.QueryParameters( + 'description', 'direction', 'ethertype', 'id', 'port_range_max', + 'port_range_min', 'protocol', 'remote_group_id', 'remote_ip_prefix', + 'security_group_id', 'tenant_id') + + # capabilities + allow_list = True + allow_create = True + allow_get = True + allow_delete = True + + # Properties + # Security group rule description. + description = resource2.Body('description') + # Direction in which the security group rule is applied. + direction = resource2.Body('direction') + # Addresses represented in CIDR must match the ingress or egress rules. + ethertype = resource2.Body('ethertype') + # Security group rule unique id. + id = resource2.Body('id') + # The maximum port number in the range that is matched + # by the security group rule. + port_range_max = resource2.Body('port_range_max', type=int) + # The minimum port number in the range that is matched + # by the security group rule. + port_range_min = resource2.Body('port_range_min', type=int) + # Protocol name or number in string format. e.g. "ICMP" or "1" + protocol = resource2.Body('protocol') + # The remote group UUID to associate with this security group rule. Only + # either one of remote_group_id and remote_ip_prefix have to be specified. + remote_group_id = resource2.Body('remote_group_id') + # The IP address prefix to associate with this security group rule. Only + # either one of remote_group_id and remote_ip_prefix have to be specified. + remote_ip_prefix = resource2.Body('remote_ip_prefix') + # Security group id. + security_group_id = resource2.Body('security_group_id') + # The owner name of security group rule. + tenant_id = resource2.Body('tenant_id') diff --git a/ecl/tests/unit/network/v2/test_port.py b/ecl/tests/unit/network/v2/test_port.py index e08bbca..d130828 100755 --- a/ecl/tests/unit/network/v2/test_port.py +++ b/ecl/tests/unit/network/v2/test_port.py @@ -12,7 +12,7 @@ import testtools -from ecl.network.v2 import port +from ecl.network.v2.port import Port IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -30,6 +30,9 @@ "mac_address": "test-mac", "name": "Example port 1", "network_id": IDENTIFIER, + "security_groups": [ + IDENTIFIER + ], "segmentation_id": 0, "segmentation_type": "flat", "tags": { @@ -44,10 +47,10 @@ class TestPort(testtools.TestCase): def test_basic(self): - sot = port.Port() + sot = Port() self.assertEqual('port', sot.resource_key) self.assertEqual('ports', sot.resources_key) - self.assertEqual('/ports', sot.base_path) + self.assertEqual('/v2.0/ports', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertTrue(sot.allow_create) self.assertTrue(sot.allow_get) @@ -56,7 +59,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) def test_make_it(self): - sot = port.Port(**EXAMPLE) + sot = Port(**EXAMPLE) self.assertTrue(sot.admin_state_up) self.assertEqual('UP', sot.admin_state) self.assertEqual(EXAMPLE['allowed_address_pairs'], @@ -68,6 +71,7 @@ def test_make_it(self): self.assertEqual(EXAMPLE['mac_address'], sot.mac_address) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['network_id'], sot.network_id) + self.assertEqual(EXAMPLE['security_groups'], sot.security_groups) self.assertEqual(EXAMPLE['segmentation_id'], sot.segmentation_id) self.assertEqual(EXAMPLE['segmentation_type'], sot.segmentation_type) self.assertEqual(EXAMPLE['tags'], sot.tags) diff --git a/ecl/tests/unit/network/v2/test_quota.py b/ecl/tests/unit/network/v2/test_quota.py index 4ed5abf..4497ede 100755 --- a/ecl/tests/unit/network/v2/test_quota.py +++ b/ecl/tests/unit/network/v2/test_quota.py @@ -12,7 +12,7 @@ import testtools -from ecl.network.v2 import quota +from ecl.network.v2.quota import Quota, QuotaDefault IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -27,18 +27,18 @@ "port": 30, "subnet": 5, "tenant_id": IDENTIFIER, - "vpn_gateway": 1 + "vpn_gateway": 1, + "security_group": 1 } - class TestQuota(testtools.TestCase): def test_basic(self): - sot = quota.Quota() + sot = Quota() self.assertEqual('quota', sot.resource_key) self.assertEqual('quotas', sot.resources_key) - self.assertEqual('/quotas', sot.base_path) + self.assertEqual('/v2.0/quotas', sot.base_path) self.assertEqual('network', sot.service.service_type) self.assertFalse(sot.allow_create) self.assertTrue(sot.allow_get) @@ -47,7 +47,7 @@ def test_basic(self): self.assertTrue(sot.allow_list) def test_make_it(self): - sot = quota.Quota(**EXAMPLE) + sot = Quota(**EXAMPLE) self.assertEqual(EXAMPLE['colocation_logical_link'], sot.colocation_logical_link) self.assertEqual(EXAMPLE['common_function_gateway'], sot.common_function_gateway) self.assertEqual(EXAMPLE['firewall'], sot.firewall) @@ -55,16 +55,17 @@ def test_make_it(self): self.assertEqual(EXAMPLE['interdc_gateway'], sot.interdc_gateway) self.assertEqual(EXAMPLE['interdc_gateway'], sot.internet_gateway) self.assertEqual(EXAMPLE['load_balancer'], sot.load_balancer) - self.assertEqual(EXAMPLE['network'], sot.networks) - self.assertEqual(EXAMPLE['port'], sot.ports) - self.assertEqual(EXAMPLE['subnet'], sot.subnets) + self.assertEqual(EXAMPLE['network'], sot.network) + self.assertEqual(EXAMPLE['port'], sot.port) + self.assertEqual(EXAMPLE['subnet'], sot.subnet) self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) self.assertEqual(EXAMPLE['vpn_gateway'], sot.vpn_gateway) + self.assertEqual(EXAMPLE['security_group'], sot.security_group) class TestQuotaDefault(testtools.TestCase): def test_basic(self): - default = quota.QuotaDefault() + default = QuotaDefault() self.assertEqual('quota', default.resource_key) self.assertEqual('quotas', default.resources_key) self.assertEqual('/quotas/%(project)s/default', default.base_path) @@ -76,7 +77,7 @@ def test_basic(self): self.assertFalse(default.allow_list) def test_make_it(self): - default = quota.QuotaDefault(**EXAMPLE) + default = QuotaDefault(**EXAMPLE) self.assertEqual(EXAMPLE['colocation_logical_link'], default.colocation_logical_link) self.assertEqual(EXAMPLE['common_function_gateway'], default.common_function_gateway) self.assertEqual(EXAMPLE['firewall'], default.firewall) @@ -84,8 +85,9 @@ def test_make_it(self): self.assertEqual(EXAMPLE['interdc_gateway'], default.interdc_gateway) self.assertEqual(EXAMPLE['interdc_gateway'], default.internet_gateway) self.assertEqual(EXAMPLE['load_balancer'], default.load_balancer) - self.assertEqual(EXAMPLE['network'], default.networks) - self.assertEqual(EXAMPLE['port'], default.ports) - self.assertEqual(EXAMPLE['subnet'], default.subnets) + self.assertEqual(EXAMPLE['network'], default.network) + self.assertEqual(EXAMPLE['port'], default.port) + self.assertEqual(EXAMPLE['subnet'], default.subnet) self.assertEqual(EXAMPLE['tenant_id'], default.project_id) self.assertEqual(EXAMPLE['vpn_gateway'], default.vpn_gateway) + self.assertEqual(EXAMPLE['security_group'], default.security_group) diff --git a/ecl/tests/unit/network/v2/test_security_group.py b/ecl/tests/unit/network/v2/test_security_group.py new file mode 100644 index 0000000..cd77239 --- /dev/null +++ b/ecl/tests/unit/network/v2/test_security_group.py @@ -0,0 +1,59 @@ +# Licensed 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 testtools + +from ecl.network.v2.security_group import SecurityGroup + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + "description": '1', + "id": IDENTIFIER, + "name": '2', + "status": '3', + "tags": { + 'tag1': '4', + 'tag2': '5' + }, + "tenant_id": IDENTIFIER, + "security_group_rules": [ + { + 'rule1': '6', + 'rule2': '7' + } + ] +} + + +class TestSecurityGroup(testtools.TestCase): + + def test_basic(self): + sot = SecurityGroup() + self.assertEqual('security_group', sot.resource_key) + self.assertEqual('security_groups', sot.resources_key) + self.assertEqual('/v2.0/security-groups', sot.base_path) + self.assertEqual('network', sot.service.service_type) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_get) + self.assertTrue(sot.allow_update) + self.assertTrue(sot.allow_delete) + + def test_make_it(self): + sot = SecurityGroup(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['name'], sot.name) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['tags'], sot.tags) + self.assertEqual(EXAMPLE['tenant_id'], sot.tenant_id) + self.assertEqual(EXAMPLE['security_group_rules'], sot.security_group_rules) diff --git a/ecl/tests/unit/network/v2/test_security_group_rule.py b/ecl/tests/unit/network/v2/test_security_group_rule.py new file mode 100644 index 0000000..9760ed6 --- /dev/null +++ b/ecl/tests/unit/network/v2/test_security_group_rule.py @@ -0,0 +1,59 @@ +# Licensed 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 testtools + +from ecl.network.v2.security_group_rule import SecurityGroupRule + +IDENTIFIER = 'IDENTIFIER' +EXAMPLE = { + 'description': '1', + 'direction': '2', + 'ethertype': '3', + 'id': IDENTIFIER, + 'port_range_min': 4, + 'port_range_max': 5, + 'protocol': '6', + 'remote_group_id': IDENTIFIER, + 'remote_ip_prefix': '7', + 'security_group_id': IDENTIFIER, + 'tenant_id': IDENTIFIER, +} + + +class TestSecurityGroupRule(testtools.TestCase): + + def test_basic(self): + sot = SecurityGroupRule() + self.assertEqual('security_group_rule', sot.resource_key) + self.assertEqual('security_group_rules', sot.resources_key) + self.assertEqual('/v2.0/security-group-rules', sot.base_path) + self.assertEqual('network', sot.service.service_type) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_get) + self.assertFalse(sot.allow_update) + self.assertTrue(sot.allow_delete) + + def test_make_it(self): + sot = SecurityGroupRule(**EXAMPLE) + self.assertEqual(EXAMPLE['description'], sot.description) + self.assertEqual(EXAMPLE['direction'], sot.direction) + self.assertEqual(EXAMPLE['ethertype'], sot.ethertype) + self.assertEqual(EXAMPLE['id'], sot.id) + self.assertEqual(EXAMPLE['port_range_min'], sot.port_range_min) + self.assertEqual(EXAMPLE['port_range_max'], sot.port_range_max) + self.assertEqual(EXAMPLE['protocol'], sot.protocol) + self.assertEqual(EXAMPLE['remote_group_id'], sot.remote_group_id) + self.assertEqual(EXAMPLE['remote_ip_prefix'], sot.remote_ip_prefix) + self.assertEqual(EXAMPLE['security_group_id'], sot.security_group_id) + self.assertEqual(EXAMPLE['tenant_id'], sot.tenant_id)