IP address acquired with associate ip address is marked as source nat (#3125)

* CLOUDSTACK-4045 added a check for network state when determining whether a new IP should be source NAT. this prevents associated IP's to be marked as source NAT when the network is in allocated state, causing disassociateIpAddress to fail later

* Remove mock object that cause other tests to fail

* Remove underscores from variable types and add documentation for the created method

* Improve exception message to include network name

* Include network UUID with the Exception message and fix failing marvin test

* Rebase against latest master and format AssociateIPAddrCmd class
This commit is contained in:
Dingane Hlaluku 2019-01-23 14:05:16 +02:00 committed by Gabriel Beims Bräscher
parent f967944d90
commit 323f791efc
6 changed files with 263 additions and 78 deletions

View File

@ -57,8 +57,12 @@ import com.cloud.offering.NetworkOffering;
import com.cloud.projects.Project;
import com.cloud.user.Account;
@APICommand(name = "associateIpAddress", description = "Acquires and associates a public IP to an account. Either of the parameters are required, i.e. either zoneId, or networkId, or vpcId ", responseObject = IPAddressResponse.class, responseView = ResponseView.Restricted,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@APICommand(name = "associateIpAddress",
description = "Acquires and associates a public IP to an account. Either of the parameters are required, i.e. either zoneId, or networkId, or vpcId ",
responseObject = IPAddressResponse.class,
responseView = ResponseView.Restricted,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class AssociateIPAddrCmd extends BaseAsyncCreateCmd {
public static final Logger s_logger = Logger.getLogger(AssociateIPAddrCmd.class.getName());
private static final String s_name = "associateipaddressresponse";
@ -67,46 +71,57 @@ public class AssociateIPAddrCmd extends BaseAsyncCreateCmd {
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "the account to associate with this IP address")
@Parameter(name = ApiConstants.ACCOUNT,
type = CommandType.STRING,
description = "the account to associate with this IP address")
private String accountName;
@Parameter(name = ApiConstants.DOMAIN_ID,
type = CommandType.UUID,
entityType = DomainResponse.class,
description = "the ID of the domain to associate with this IP address")
type = CommandType.UUID,
entityType = DomainResponse.class,
description = "the ID of the domain to associate with this IP address")
private Long domainId;
@Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.UUID,
entityType = ZoneResponse.class,
description = "the ID of the availability zone you want to acquire an public IP address from")
type = CommandType.UUID,
entityType = ZoneResponse.class,
description = "the ID of the availability zone you want to acquire an public IP address from")
private Long zoneId;
@Parameter(name = ApiConstants.NETWORK_ID,
type = CommandType.UUID,
entityType = NetworkResponse.class,
description = "The network this IP address should be associated to.")
type = CommandType.UUID,
entityType = NetworkResponse.class,
description = "The network this IP address should be associated to.")
private Long networkId;
@Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "Deploy VM for the project")
@Parameter(name = ApiConstants.PROJECT_ID,
type = CommandType.UUID,
entityType = ProjectResponse.class,
description = "Deploy VM for the project")
private Long projectId;
@Parameter(name = ApiConstants.VPC_ID, type = CommandType.UUID, entityType = VpcResponse.class, description = "the VPC you want the IP address to "
+ "be associated with")
@Parameter(name = ApiConstants.VPC_ID,
type = CommandType.UUID,
entityType = VpcResponse.class,
description = "the VPC you want the IP address to be associated with")
private Long vpcId;
@Parameter(name = ApiConstants.IS_PORTABLE, type = BaseCmd.CommandType.BOOLEAN, description = "should be set to true "
+ "if public IP is required to be transferable across zones, if not specified defaults to false")
@Parameter(name = ApiConstants.IS_PORTABLE,
type = BaseCmd.CommandType.BOOLEAN,
description = "should be set to true if public IP is required to be transferable across zones, if not specified defaults to false")
private Boolean isPortable;
@Parameter(name = ApiConstants.REGION_ID,
type = CommandType.INTEGER,
entityType = RegionResponse.class,
required = false,
description = "region ID from where portable IP is to be associated.")
type = CommandType.INTEGER,
entityType = RegionResponse.class,
required = false,
description = "region ID from where portable IP is to be associated.")
private Integer regionId;
@Parameter(name = ApiConstants.FOR_DISPLAY, type = CommandType.BOOLEAN, description = "an optional field, whether to the display the IP to the end user or not", since = "4.4", authorized = {RoleType.Admin})
@Parameter(name = ApiConstants.FOR_DISPLAY,
type = CommandType.BOOLEAN,
description = "an optional field, whether to the display the IP to the end user or not", since = "4.4",
authorized = {RoleType.Admin})
private Boolean display;
/////////////////////////////////////////////////////
@ -178,7 +193,7 @@ public class AssociateIPAddrCmd extends BaseAsyncCreateCmd {
if (networks.size() == 0) {
String domain = _domainService.getDomain(getDomainId()).getName();
throw new InvalidParameterValueException("Account name=" + getAccountName() + " domain=" + domain + " doesn't have virtual networks in zone=" +
zone.getName());
zone.getName());
}
if (networks.size() < 1) {
@ -205,7 +220,7 @@ public class AssociateIPAddrCmd extends BaseAsyncCreateCmd {
@Override
public boolean isDisplay() {
if(display == null)
if (display == null)
return true;
else
return display;
@ -224,7 +239,7 @@ public class AssociateIPAddrCmd extends BaseAsyncCreateCmd {
return project.getProjectAccountId();
} else {
throw new PermissionDeniedException("Can't add resources to the project with specified projectId in state=" + project.getState() +
" as it's no longer active");
" as it's no longer active");
}
} else {
throw new InvalidParameterValueException("Unable to find project by ID");
@ -268,7 +283,7 @@ public class AssociateIPAddrCmd extends BaseAsyncCreateCmd {
@Override
public String getEventDescription() {
return "associating IP to network ID: " + getNetworkId() + " in zone " + getZoneId();
return "associating IP to network ID: " + getNetworkId() + " in zone " + getZoneId();
}
/////////////////////////////////////////////////////

View File

@ -1377,16 +1377,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
}
}
NetworkOffering offering = _networkOfferingDao.findById(network.getNetworkOfferingId());
boolean sharedSourceNat = offering.isSharedSourceNat();
boolean isSourceNat = false;
if (!sharedSourceNat) {
if (getExistingSourceNatInNetwork(owner.getId(), networkId) == null) {
if (network.getGuestType() == GuestType.Isolated && network.getVpcId() == null && !ipToAssoc.isPortable()) {
isSourceNat = true;
}
}
}
boolean isSourceNat = isSourceNatAvailableForNetwork(owner, ipToAssoc, network);
s_logger.debug("Associating ip " + ipToAssoc + " to network " + network);
@ -1424,6 +1415,33 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage
}
}
/**
* Prevents associating an IP address to an allocated (unimplemented network) network, throws an Exception otherwise
* @param owner Used to check if the user belongs to the Network
* @param ipToAssoc IP address to be associated to a Network, can only be associated to an implemented network for Source NAT
* @param network Network to which IP address is to be associated with, must not be in allocated state for Source NAT Network/IP association
* @return true if IP address can be successfully associated with Source NAT network
*/
protected boolean isSourceNatAvailableForNetwork(Account owner, IPAddressVO ipToAssoc, Network network) {
NetworkOffering offering = _networkOfferingDao.findById(network.getNetworkOfferingId());
boolean sharedSourceNat = offering.isSharedSourceNat();
boolean isSourceNat = false;
if (!sharedSourceNat) {
if (getExistingSourceNatInNetwork(owner.getId(), network.getId()) == null) {
if (network.getGuestType() == GuestType.Isolated && network.getVpcId() == null && !ipToAssoc.isPortable()) {
if (network.getState() == Network.State.Allocated) {
//prevent associating an ip address to an allocated (unimplemented network).
//it will cause the ip to become source nat, and it can't be disassociated later on.
String msg = String.format("Network with UUID:%s is in allocated and needs to be implemented first before acquiring an IP address", network.getUuid());
throw new InvalidParameterValueException(msg);
}
isSourceNat = true;
}
}
}
return isSourceNat;
}
protected boolean isSharedNetworkOfferingWithServices(long networkOfferingId) {
NetworkOfferingVO networkOffering = _networkOfferingDao.findById(networkOfferingId);
if ((networkOffering.getGuestType() == Network.GuestType.Shared)

View File

@ -17,7 +17,10 @@
package com.cloud.network;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -28,32 +31,63 @@ import java.util.List;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.Network.Service;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.rules.StaticNat;
import com.cloud.network.rules.StaticNatImpl;
import com.cloud.offerings.NetworkOfferingVO;
import com.cloud.offerings.dao.NetworkOfferingDao;
import com.cloud.user.AccountVO;
import com.cloud.utils.net.Ip;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class IpAddressManagerTest {
@Mock
IPAddressDao _ipAddrDao;
IPAddressDao ipAddressDao;
@Mock
NetworkDao networkDao;
@Mock
NetworkOfferingDao networkOfferingDao;
@Spy
@InjectMocks
IpAddressManagerImpl _ipManager;
IpAddressManagerImpl ipAddressManager;
@InjectMocks
NetworkModelImpl networkModel = Mockito.spy(new NetworkModelImpl());
IPAddressVO ipAddressVO;
AccountVO account;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
public void setup() throws ResourceUnavailableException {
ipAddressVO = new IPAddressVO(new Ip("192.0.0.1"), 1L, 1L, 1L,false);
ipAddressVO.setAllocatedToAccountId(1L);
account = new AccountVO("admin", 1L, null, (short) 1, 1L, "c65a73d5-ebbd-11e7-8f45-107b44277808");
account.setId(1L);
NetworkOfferingVO networkOfferingVO = Mockito.mock(NetworkOfferingVO.class);
networkOfferingVO.setSharedSourceNat(false);
Mockito.when(networkOfferingDao.findById(Mockito.anyLong())).thenReturn(networkOfferingVO);
}
@Test
@ -63,10 +97,10 @@ public class IpAddressManagerTest {
when(vo.getAddress()).thenReturn(new Ip(publicIpAddress));
when(vo.getId()).thenReturn(1l);
when(_ipAddrDao.findById(anyLong())).thenReturn(vo);
when(ipAddressDao.findById(anyLong())).thenReturn(vo);
StaticNat snat = new StaticNatImpl(1, 1, 1, 1, publicIpAddress, false);
List<IPAddressVO> ips = _ipManager.getStaticNatSourceIps(Collections.singletonList(snat));
List<IPAddressVO> ips = ipAddressManager.getStaticNatSourceIps(Collections.singletonList(snat));
Assert.assertNotNull(ips);
Assert.assertEquals(1, ips.size());
@ -78,7 +112,7 @@ public class IpAddressManagerTest {
public void isIpEqualsGatewayOrNetworkOfferingsEmptyTestRequestedIpEqualsIp6Gateway() {
Network network = setTestIsIpEqualsGatewayOrNetworkOfferingsEmpty(0l, "gateway", "ip6Gateway", null, new ArrayList<Service>());
boolean result = _ipManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "ip6Gateway");
boolean result = ipAddressManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "ip6Gateway");
Mockito.verify(networkModel, Mockito.times(0)).listNetworkOfferingServices(Mockito.anyLong());
Assert.assertTrue(result);
@ -88,7 +122,7 @@ public class IpAddressManagerTest {
public void isIpEqualsGatewayOrNetworkOfferingsEmptyTestRequestedIpEqualsGateway() {
Network network = setTestIsIpEqualsGatewayOrNetworkOfferingsEmpty(0l, "gateway", "ip6Gateway", null, new ArrayList<Service>());
boolean result = _ipManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "gateway");
boolean result = ipAddressManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "gateway");
Mockito.verify(networkModel, Mockito.times(0)).listNetworkOfferingServices(Mockito.anyLong());
Assert.assertTrue(result);
@ -101,7 +135,7 @@ public class IpAddressManagerTest {
services.add(serviceGateway);
Network network = setTestIsIpEqualsGatewayOrNetworkOfferingsEmpty(0l, "gateway", "ip6Gateway", null, services);
boolean result = _ipManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "requestedIp");
boolean result = ipAddressManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "requestedIp");
Mockito.verify(networkModel).listNetworkOfferingServices(Mockito.anyLong());
Assert.assertFalse(result);
@ -111,17 +145,75 @@ public class IpAddressManagerTest {
public void isIpEqualsGatewayOrNetworkOfferingsEmptyTestExpectFalseServicesCidrNotNull() {
Network network = setTestIsIpEqualsGatewayOrNetworkOfferingsEmpty(0l, "gateway", "ip6Gateway", "cidr", new ArrayList<Service>());
boolean result = _ipManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "requestedIp");
boolean result = ipAddressManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "requestedIp");
Mockito.verify(networkModel).listNetworkOfferingServices(Mockito.anyLong());
Assert.assertFalse(result);
}
@Test
public void assertSourceNatImplementedNetwork() {
NetworkVO networkImplemented = Mockito.mock(NetworkVO.class);
when(networkImplemented.getTrafficType()).thenReturn(Networks.TrafficType.Guest);
when(networkImplemented.getNetworkOfferingId()).thenReturn(8L);
when(networkImplemented.getState()).thenReturn(Network.State.Implemented);
when(networkImplemented.getGuestType()).thenReturn(Network.GuestType.Isolated);
when(networkImplemented.getVpcId()).thenReturn(null);
when(networkImplemented.getId()).thenReturn(1L);
Mockito.when(networkDao.findById(1L)).thenReturn(networkImplemented);
doReturn(null).when(ipAddressManager).getExistingSourceNatInNetwork(1L, 1L);
boolean isSourceNat = ipAddressManager.isSourceNatAvailableForNetwork(account, ipAddressVO, networkImplemented);
assertTrue("Source NAT should be true", isSourceNat);
}
@Test(expected = InvalidParameterValueException.class)
public void assertSourceNatAllocatedNetwork() {
NetworkVO networkAllocated = Mockito.mock(NetworkVO.class);
when(networkAllocated.getTrafficType()).thenReturn(Networks.TrafficType.Guest);
when(networkAllocated.getNetworkOfferingId()).thenReturn(8L);
when(networkAllocated.getState()).thenReturn(Network.State.Allocated);
when(networkAllocated.getGuestType()).thenReturn(Network.GuestType.Isolated);
when(networkAllocated.getVpcId()).thenReturn(null);
when(networkAllocated.getId()).thenReturn(2L);
Mockito.when(networkDao.findById(2L)).thenReturn(networkAllocated);
doReturn(null).when(ipAddressManager).getExistingSourceNatInNetwork(1L, 2L);
ipAddressManager.isSourceNatAvailableForNetwork(account, ipAddressVO, networkAllocated);
}
@Test
public void assertExistingSourceNatAllocatedNetwork() {
NetworkVO networkNat = Mockito.mock(NetworkVO.class);
when(networkNat.getTrafficType()).thenReturn(Networks.TrafficType.Guest);
when(networkNat.getNetworkOfferingId()).thenReturn(8L);
when(networkNat.getState()).thenReturn(Network.State.Implemented);
when(networkNat.getGuestType()).thenReturn(Network.GuestType.Isolated);
when(networkNat.getId()).thenReturn(3L);
when(networkNat.getVpcId()).thenReturn(null);
when(networkNat.getId()).thenReturn(3L);
IPAddressVO sourceNat = new IPAddressVO(new Ip("192.0.0.2"), 1L, 1L, 1L,true);
Mockito.when(networkDao.findById(3L)).thenReturn(networkNat);
doReturn(sourceNat).when(ipAddressManager).getExistingSourceNatInNetwork(1L, 3L);
boolean isSourceNat = ipAddressManager.isSourceNatAvailableForNetwork(account, ipAddressVO, networkNat);
assertFalse("Source NAT should be false", isSourceNat);
}
@Test
public void isIpEqualsGatewayOrNetworkOfferingsEmptyTestNetworkOfferingsEmptyAndCidrNull() {
Network network = setTestIsIpEqualsGatewayOrNetworkOfferingsEmpty(0l, "gateway", "ip6Gateway", null, new ArrayList<Service>());
boolean result = _ipManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "requestedIp");
boolean result = ipAddressManager.isIpEqualsGatewayOrNetworkOfferingsEmpty(network, "requestedIp");
Mockito.verify(networkModel).listNetworkOfferingServices(Mockito.anyLong());
Assert.assertTrue(result);

View File

@ -2516,6 +2516,21 @@ class TestResourceTags(cloudstackTestCase):
domainid=self.child_do_admin.domainid,
zoneid=self.zone.id
)
tag = "tag1"
self.so_with_tag = ServiceOffering.create(
self.apiclient,
self.services["service_offering"],
hosttags=tag
)
self.vm = VirtualMachine.create(
self.api_client,
self.services["virtual_machine"],
accountid=self.child_do_admin.name,
domainid=self.child_do_admin.domainid,
networkids=self.network.id,
serviceofferingid=self.so_with_tag.id
)
self.debug("Fetching the network details for account: %s" %
self.child_do_admin.name
)

View File

@ -110,6 +110,42 @@ class TestPublicIP(cloudstackTestCase):
cls.user.domainid
)
cls.service_offering = ServiceOffering.create(
cls.apiclient,
cls.services["service_offerings"]["tiny"],
)
cls.hypervisor = testClient.getHypervisorInfo()
cls.template = get_test_template(
cls.apiclient,
cls.zone.id,
cls.hypervisor
)
if cls.template == FAILED:
assert False, "get_test_template() failed to return template"
cls.services["virtual_machine"]["zoneid"] = cls.zone.id
cls.account_vm = VirtualMachine.create(
cls.apiclient,
cls.services["virtual_machine"],
templateid=cls.template.id,
accountid=cls.account.name,
domainid=cls.account.domainid,
networkids=cls.account_network.id,
serviceofferingid=cls.service_offering.id
)
cls.user_vm = VirtualMachine.create(
cls.apiclient,
cls.services["virtual_machine"],
templateid=cls.template.id,
accountid=cls.user.name,
domainid=cls.user.domainid,
networkids=cls.user_network.id,
serviceofferingid=cls.service_offering.id
)
# Create Source NAT IP addresses
PublicIPAddress.create(
cls.apiclient,
@ -124,6 +160,8 @@ class TestPublicIP(cloudstackTestCase):
cls.user.domainid
)
cls._cleanup = [
cls.account_vm,
cls.user_vm,
cls.account_network,
cls.user_network,
cls.account,

View File

@ -16,33 +16,23 @@
# under the License.
# Import Local Modules
from marvin.cloudstackTestCase import cloudstackTestCase, unittest
from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.codes import PASS
from marvin.lib.base import (PublicIPAddress,
NetworkOffering,
Autoscale,
Network,
NetworkServiceProvider,
Template,
VirtualMachine,
VPC,
VpcOffering,
StaticNATRule,
FireWallRule,
NATRule,
Vpn,
VpnUser,
LoadBalancerRule,
Account,
ServiceOffering,
PhysicalNetwork,
User)
ServiceOffering)
from marvin.lib.common import (get_domain,
get_zone,
get_test_template)
from marvin.lib.utils import validateList, cleanup_resources
from marvin.codes import PASS
from nose.plugins.attrib import attr
class TestPortForwardingRules(cloudstackTestCase):
@classmethod
@ -121,6 +111,7 @@ class TestPortForwardingRules(cloudstackTestCase):
cleanup_resources(cls.api_client, cls._cleanup)
except Exception as e:
raise Exception("Warning: Exception during cleanup : %s" % e)
return
def __verify_values(self, expected_vals, actual_vals):
"""
@ -153,8 +144,6 @@ class TestPortForwardingRules(cloudstackTestCase):
(exp_val, act_val))
return return_flag
@attr(tags=["advanced"], required_hardware="true")
def test_01_create_delete_portforwarding_fornonvpc(self):
"""
@ -213,6 +202,7 @@ class TestPortForwardingRules(cloudstackTestCase):
networkofferingid=network_offerings_list[0].id,
zoneid=self.zone.id
)
self.assertIsNotNone(
network,
"Network creation failed"
@ -227,6 +217,25 @@ class TestPortForwardingRules(cloudstackTestCase):
list_ipaddresses_before,
"IP Addresses listed for newly created User"
)
service_offering = ServiceOffering.create(
self.apiClient,
self.services["service_offerings"]["tiny"],
)
self.services["virtual_machine"]["zoneid"] = self.zone.id
vm = VirtualMachine.create(
self.userapiclient,
self.services["virtual_machine"],
accountid=self.account.name,
domainid=self.account.domainid,
networkids=network.id,
serviceofferingid=service_offering.id
)
VirtualMachine.delete(vm, self.apiClient, expunge=True)
# Associating an IP Addresses to Network created
associated_ipaddress = PublicIPAddress.create(
self.userapiclient,
@ -248,9 +257,9 @@ class TestPortForwardingRules(cloudstackTestCase):
status[0],
"IP Addresses Association Failed"
)
# Verifying the length of the list is 1
# Verifying the length of the list is 2
self.assertEqual(
1,
2,
len(list_ipaddresses_after),
"Number of IP Addresses associated are not matching expected"
)
@ -281,9 +290,9 @@ class TestPortForwardingRules(cloudstackTestCase):
status[0],
"VM Created is not in Running state"
)
# Verifying the length of the list is 1
# Verifying the length of the list is 2
self.assertEqual(
1,
2,
len(list_ipaddresses_after),
"VM Created is not in Running state"
)
@ -370,35 +379,34 @@ class TestPortForwardingRules(cloudstackTestCase):
# Deleting Port Forwarding Rule
portfwd_rule.delete(self.userapiclient)
# Creating a Port Forwarding rule with port range
portfwd_rule = NATRule.create(
self.userapiclient,
virtual_machine=vm_created,
services=self.services["natrulerange"],
ipaddressid=associated_ipaddress.ipaddress.id,
)
)
self.assertIsNotNone(
portfwd_rule,
"Failed to create Port Forwarding Rule"
)
#update the private port for port forwarding rule
# update the private port for port forwarding rule
updatefwd_rule = portfwd_rule.update(self.userapiclient,
portfwd_rule.id,
virtual_machine=vm_created,
services=self.services["updatenatrulerange"],
)
portfwd_rule.id,
virtual_machine=vm_created,
services=self.services["updatenatrulerange"],
)
# Verifying details of Sticky Policy created
# Creating expected and actual values dictionaries
expected_dict = {
"privateport": str(self.services["updatenatrulerange"]["privateport"]),
"privateendport": str(self.services["updatenatrulerange"]["privateendport"]),
}
}
actual_dict = {
"privateport": str(updatefwd_rule.privateport),
"privateendport": str(updatefwd_rule.privateendport),
}
}
portfwd_status = self.__verify_values(
expected_dict,
actual_dict
@ -425,4 +433,3 @@ class TestPortForwardingRules(cloudstackTestCase):
vm_created.delete(self.apiClient)
self.cleanup.append(self.account)
return