From ff72492c01f9f5f7f7e1f74253b0439267162a12 Mon Sep 17 00:00:00 2001 From: Likitha Shetty Date: Tue, 22 May 2012 10:59:57 +0530 Subject: [PATCH 01/14] CS-15031. ec2-describe-instances, provide support for group-id filter. --- .../bridge/service/core/ec2/EC2InstanceFilterSet.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/awsapi/src/com/cloud/bridge/service/core/ec2/EC2InstanceFilterSet.java b/awsapi/src/com/cloud/bridge/service/core/ec2/EC2InstanceFilterSet.java index d0a44de0e0b..29cdecda2b7 100644 --- a/awsapi/src/com/cloud/bridge/service/core/ec2/EC2InstanceFilterSet.java +++ b/awsapi/src/com/cloud/bridge/service/core/ec2/EC2InstanceFilterSet.java @@ -46,6 +46,7 @@ public class EC2InstanceFilterSet { filterTypes.put( "owner-id", "string" ); filterTypes.put( "root-device-name", "string" ); filterTypes.put( "private-ip-address", "string" ); + filterTypes.put( "group-id", "string" ); } @@ -154,6 +155,13 @@ public class EC2InstanceFilterSet { { return containsDevice( vm.getRootDeviceId(), valueSet ); } + else if (filterName.equalsIgnoreCase( "group-id")) + { + String[] groupSet = vm.getGroupSet(); + for (String group : groupSet) + if (containsString(group, valueSet)) return true; + return false; + } else return false; } From a92a9f020c3f7f334d60b67a1c27a2ee80b9e4d4 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 22 May 2012 11:09:12 +0530 Subject: [PATCH 02/14] CS-15040: Do not package the css, js, ui and images directory in the agent package --- debian/cloud-agent.install | 4 ---- 1 file changed, 4 deletions(-) diff --git a/debian/cloud-agent.install b/debian/cloud-agent.install index 5a053116690..5ef72ce9d75 100644 --- a/debian/cloud-agent.install +++ b/debian/cloud-agent.install @@ -5,8 +5,4 @@ /etc/init.d/cloud-agent /usr/bin/agent-runner /usr/bin/cloud-setup-agent -/usr/lib/cloud/agent/css -/usr/lib/cloud/agent/ui -/usr/lib/cloud/agent/js -/usr/lib/cloud/agent/images /var/log/cloud/agent From 5de593500b8451466be3ce0fa408deeb460b84cc Mon Sep 17 00:00:00 2001 From: Chirag Jog Date: Tue, 22 May 2012 18:17:57 +0530 Subject: [PATCH 03/14] Adding ELP/EIP tests & firewall rule changes for SSH --- .../component/test_egress_rules.py | 34 +- test/integration/component/test_eip_elb.py | 1574 +++++++++++++++++ .../component/test_high_availability.py | 197 ++- .../component/test_network_offering.py | 56 +- .../component/test_project_resources.py | 3 +- test/integration/component/test_projects.py | 2 +- .../component/test_resource_limits.py | 1 + test/integration/component/test_routers.py | 48 +- .../component/test_security_groups.py | 4 +- test/integration/component/test_snapshots.py | 2 +- test/integration/component/test_templates.py | 1 + test/integration/component/test_volumes.py | 2 +- test/integration/lib/base.py | 473 +++-- test/integration/lib/common.py | 102 +- test/integration/lib/utils.py | 32 +- test/integration/smoke/test_network.py | 257 +-- test/integration/smoke/test_routers.py | 3 +- test/integration/smoke/test_snapshots.py | 4 +- test/integration/smoke/test_templates.py | 7 +- test/integration/smoke/test_vm_life_cycle.py | 350 +++- test/integration/smoke/test_volumes.py | 6 +- 21 files changed, 2673 insertions(+), 485 deletions(-) create mode 100644 test/integration/component/test_eip_elb.py diff --git a/test/integration/component/test_egress_rules.py b/test/integration/component/test_egress_rules.py index d0e00498ac6..aa301dad3cb 100644 --- a/test/integration/component/test_egress_rules.py +++ b/test/integration/component/test_egress_rules.py @@ -34,7 +34,7 @@ class Services: def __init__(self): self.services = { - "disk_offering": { + "disk_offering":{ "displaytext": "Small", "name": "Small", "disksize": 1 @@ -64,8 +64,8 @@ class Services: "name": "Tiny Instance", "displaytext": "Tiny Instance", "cpunumber": 1, - "cpuspeed": 100, # in MHz - "memory": 64, # In MBs + "cpuspeed": 100, # in MHz + "memory": 64, # In MBs }, "security_group": { "name": 'SSH', @@ -107,9 +107,8 @@ class Services: "protocol": 'TCP', "startport": 22, "endport": 22, - "cidrlist": '0.0.0.0/0', }, - "mgmt_server": { + "mgmt_server": { "username": "root", "password": "fr3sca", "ipaddress": "192.168.100.21" @@ -118,7 +117,7 @@ class Services: # CentOS 5.3 (64-bit) "sleep": 60, "timeout": 10, - "mode": 'basic', + "mode":'basic', # Networking mode: Basic or Advanced } @@ -801,7 +800,7 @@ class TestDefaultGroupEgressAfterDeploy(cloudstackTestCase): # --- www.l.google.com ping statistics --- # 1 packets transmitted, 1 received, 0% packet loss, time 0ms # rtt min/avg/max/mdev = 25.970/25.970/25.970/0.000 ms - self.debug("SSH result: %s" %str(res)) + self.debug("SSH result: %s" % str(res)) except Exception as e: self.fail("SSH Access failed for %s: %s" % \ (self.virtual_machine.ipaddress, e) @@ -985,7 +984,7 @@ class TestRevokeEgressRule(cloudstackTestCase): # --- www.l.google.com ping statistics --- # 1 packets transmitted, 1 received, 0% packet loss, time 0ms # rtt min/avg/max/mdev = 25.970/25.970/25.970/0.000 ms - self.debug("SSH result: %s" % str(res)) + self.debug("SSH result: %s" % str(res)) except Exception as e: self.fail("SSH Access failed for %s: %s" % \ (self.virtual_machine.ipaddress, e) @@ -1026,7 +1025,7 @@ class TestRevokeEgressRule(cloudstackTestCase): result = security_group.revokeEgress( self.apiclient, - id=ssh_egress_rule["ruleid"] + id = ssh_egress_rule["ruleid"] ) self.debug("Revoke egress rule result: %s" % result) @@ -1319,14 +1318,14 @@ class TestMultipleAccountsEgressRuleNeg(cloudstackTestCase): "Authorizing egress rule for sec group ID: %s for ssh access" % security_group.id) # Authorize to only account not CIDR - user_secgrp_list = {self.accountB.account.name: 'default'} + user_secgrp_list = {self.accountB.account.name: 'default'} egress_rule = security_group.authorizeEgress( self.apiclient, self.services["sg_account"], account=self.accountA.account.name, domainid=self.accountA.account.domainid, - user_secgrp_list=user_secgrp_list + user_secgrp_list=user_secgrp_list ) self.assertEqual( @@ -1422,7 +1421,7 @@ class TestMultipleAccountsEgressRuleNeg(cloudstackTestCase): try: self.debug("SSHing into VM type B from VM A") - self.debug("VM IP: %s" % self.virtual_machineB.ssh_ip) + self.debug("VM IP: %s" % self.virtual_machineB.ssh_ip) res = ssh.execute("ssh %s@%s" % ( self.services["virtual_machine"]["username"], self.virtual_machineB.ssh_ip @@ -1592,14 +1591,14 @@ class TestMultipleAccountsEgressRule(cloudstackTestCase): "Authorizing egress rule for sec group ID: %s for ssh access" % security_groupA.id) # Authorize to only account not CIDR - user_secgrp_list = {self.accountB.account.name: security_groupB.name} + user_secgrp_list = {self.accountB.account.name: security_groupB.name} egress_rule = security_groupA.authorizeEgress( self.apiclient, self.services["sg_account"], account=self.accountA.account.name, domainid=self.accountA.account.domainid, - user_secgrp_list=user_secgrp_list + user_secgrp_list=user_secgrp_list ) self.assertEqual( @@ -1716,7 +1715,7 @@ class TestMultipleAccountsEgressRule(cloudstackTestCase): try: self.debug("SSHing into VB type B from VM A") - self.debug("VM IP: %s" % self.virtual_machineB.ssh_ip) + self.debug("VM IP: %s" % self.virtual_machineB.ssh_ip) res = ssh.execute("ssh %s@%s" % ( self.services["virtual_machine"]["username"], @@ -1945,7 +1944,6 @@ class TestStartStopVMWithEgressRule(cloudstackTestCase): ) return - @unittest.skip("Valid bug- ID: CS-12647") class TestInvalidParametersForEgress(cloudstackTestCase): @@ -2306,12 +2304,12 @@ class TestEgressAfterHostMaintainance(cloudstackTestCase): ) vm = vms[0] - self.debug("Enabling host maintainance for ID: %s" % vm.hostid) + self.debug("Enabling host maintainance for ID: %s" % host.id) cmd = prepareHostForMaintenance.prepareHostForMaintenanceCmd() cmd.id = vm.hostid self.apiclient.prepareHostForMaintenance(cmd) - self.debug("Canceling host maintainance for ID: %s" % vm.hostid) + self.debug("Canceling host maintainance for ID: %s" % host.id) cmd = cancelHostMaintenance.cancelHostMaintenanceCmd() cmd.id = vm.hostid self.apiclient.cancelHostMaintenance(cmd) diff --git a/test/integration/component/test_eip_elb.py b/test/integration/component/test_eip_elb.py new file mode 100644 index 00000000000..ac11d6dab54 --- /dev/null +++ b/test/integration/component/test_eip_elb.py @@ -0,0 +1,1574 @@ +# -*- encoding: utf-8 -*- +# Copyright 2012 Citrix Systems, Inc. Licensed under the +# Apache License, Version 2.0 (the "License"); you may not use this +# file except in compliance with the License. Citrix Systems, Inc. +# reserves all rights not expressly granted by 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. +# +# Automatically generated by addcopyright.py at 04/03/2012 + +""" P1 tests for elastic load balancing and elastic IP +""" +#Import Local Modules +import marvin +from marvin.cloudstackTestCase import * +from marvin.cloudstackAPI import * +from integration.lib.utils import * +from integration.lib.base import * +from integration.lib.common import * +from marvin.remoteSSHClient import remoteSSHClient +import datetime + + +class Services: + """Test elastic load balancing and elastic IP + """ + + def __init__(self): + self.services = { + "account": { + "email": "test@test.com", + "firstname": "Test", + "lastname": "User", + "username": "test", + # Random characters are appended for unique + # username + "password": "fr3sca", + }, + "service_offering": { + "name": "Tiny Instance", + "displaytext": "Tiny Instance", + "cpunumber": 1, + "cpuspeed": 100, # in MHz + "memory": 64, # In MBs + }, + "lbrule": { + "name": "SSH", + "alg": "roundrobin", + # Algorithm used for load balancing + "privateport": 22, + "publicport": 22, + "openfirewall": False, + }, + "natrule": { + "privateport": 22, + "publicport": 22, + "protocol": "TCP" + }, + "virtual_machine": { + "displayname": "Test VM", + "username": "root", + "password": "password", + "ssh_port": 22, + "hypervisor": 'XenServer', + # Hypervisor type should be same as + # hypervisor type of cluster + "privateport": 22, + "publicport": 22, + "protocol": 'TCP', + }, + "netscaler": { + "ipaddress": '192.168.100.213', + "username": 'nsroot', + "password": 'nsroot' + }, + "ostypeid": 'd73dd44c-4244-4848-b20a-906796326749', + # Cent OS 5.3 (64 bit) + "sleep": 60, + "timeout": 10, + "mode": 'basic' + } + + +class TestEIP(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.api_client = super(TestEIP, cls).getClsTestClient().getApiClient() + cls.services = Services().services + # Get Zone, Domain and templates + cls.domain = get_domain(cls.api_client, cls.services) + cls.zone = get_zone(cls.api_client, cls.services) + cls.template = get_template( + cls.api_client, + cls.zone.id, + cls.services["ostypeid"] + ) + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + cls.services["virtual_machine"]["template"] = cls.template.id + + cls.service_offering = ServiceOffering.create( + cls.api_client, + cls.services["service_offering"] + ) + cls.account = Account.create( + cls.api_client, + cls.services["account"], + admin=True, + domainid=cls.domain.id + ) + # Spawn an instance + cls.virtual_machine = VirtualMachine.create( + cls.api_client, + cls.services["virtual_machine"], + accountid=cls.account.account.name, + domainid=cls.account.account.domainid, + serviceofferingid=cls.service_offering.id + ) + networks = Network.list( + cls.api_client, + zoneid=cls.zone.id, + listall=True + ) + if isinstance(networks, list): + # Basic zone has only one network i.e Basic network + cls.guest_network = networks[0] + else: + raise Exception( + "List networks returned empty response for zone: %s" % + cls.zone.id) + + ip_addrs = PublicIPAddress.list( + cls.api_client, + associatednetworkid=cls.guest_network.id, + isstaticnat=True, + account=cls.account.account.name, + domainid=cls.account.account.domainid, + listall=True + ) + if isinstance(ip_addrs, list): + cls.source_nat = ip_addrs[0] + else: + raise Exception( + "No Source NAT IP found for guest network: %s" % + guest_network.id) + cls._cleanup = [ + cls.account, + cls.service_offering, + ] + return + + @classmethod + def tearDownClass(cls): + try: + #Cleanup resources used + cleanup_resources(cls.api_client, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + return + + def tearDown(self): + try: + #Clean up, terminate the created network offerings + cleanup_resources(self.apiclient, self.cleanup) + self.dbclient.close() + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def test_01_eip_by_deploying_instance(self): + """Test EIP by deploying an instance + """ + + # Validate the following + # 1. Instance gets an IP from GUEST IP range. + # 2. One IP from EIP pool is taken and configured on NS + # commands to verify on NS: + # show ip, show inat- make sure that output says USIP : ON + # 3. After allowing ingress rule based on source CIDR to the + # respective port, verify that you are able to reach guest with EIP + # 4. user_ip_address.is_system=1, user_ip_address.one_to_one_nat=1 + + self.debug("Fetching public network IP range for public network") + ip_ranges = PublicIpRange.list( + self.apiclient, + zoneid=self.zone.id, + forvirtualnetwork=True + ) + self.assertEqual( + isinstance(ip_ranges, list), + True, + "Public IP range should return a valid range" + ) + # Guest network can have multiple IP ranges. In that case, split IP + # address and then compare the values + for ip_range in ip_ranges: + self.debug("IP range: %s - %s" % ( + ip_range.startip, + ip_range.endip + )) + + start_ip_list = ip_range.startip.split(".") + end_ip_list = ip_range.endip.split(".") + source_nat_list = self.source_nat.ipaddress.split(".") + + flag = True + for i in range(3): + if int(start_ip_list[i]) != int(source_nat_list[i]): + flag = False + + self.assertGreaterEqual( + int(source_nat_list[3]), + int(start_ip_list[3]), + "The NAT should be greater/equal to start IP of guest network" + ) + + self.assertLessEqual( + int(source_nat_list[3]), + int(end_ip_list[3]), + "The NAT should be less/equal to start IP of guest network" + ) + + # Verify listSecurity groups response + security_groups = SecurityGroup.list( + self.apiclient, + account=self.account.account.name, + domainid=self.account.account.domainid + ) + self.assertEqual( + isinstance(security_groups, list), + True, + "Check for list security groups response" + ) + self.assertEqual( + len(security_groups), + 1, + "Check List Security groups response" + ) + self.debug("List Security groups response: %s" % + str(security_groups)) + + security_group = security_groups[0] + + self.debug( + "Creating Ingress rule to allow SSH on default security group") + + cmd = authorizeSecurityGroupIngress.authorizeSecurityGroupIngressCmd() + cmd.domainid = self.account.account.domainid + cmd.account = self.account.account.name + cmd.securitygroupid = security_group.id + cmd.protocol = 'TCP' + cmd.startport = 22 + cmd.endport = 22 + cmd.cidrlist = '0.0.0.0/0' + self.apiclient.authorizeSecurityGroupIngress(cmd) + + try: + self.debug("SSH into VM: %s" % self.virtual_machine.ssh_ip) + ssh = self.virtual_machine.get_ssh_client( + ipaddress=self.source_nat.ipaddress) + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (self.virtual_machine.ipaddress, e) + ) + # Fetch details from user_ip_address table in database + self.debug( + "select is_system, one_to_one_nat from user_ip_address where public_ip_address='%s';" \ + % self.source_nat.ipaddress) + + qresultset = self.dbclient.execute( + "select is_system, one_to_one_nat from user_ip_address where public_ip_address='%s';" \ + % self.source_nat.ipaddress + ) + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + 1, + "user_ip_address.is_system value should be 1 for static NAT" + ) + + self.assertEqual( + qresult[1], + 1, + "user_ip_address.one_to_one_nat value should be 1 for static NAT" + ) + self.debug("SSH into netscaler: %s" % + self.services["netscaler"]["ipaddress"]) + try: + ssh_client = remoteSSHClient.remoteSSHClient( + self.services["netscaler"]["ipaddress"], + 22, + self.services["netscaler"]["username"], + self.services["netscaler"]["password"], + ) + self.debug("command: show ip") + res = ssh_client.execute("show ip") + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count(self.source_nat.ipaddress), + 1, + "One IP from EIP pool should be taken and configured on NS" + ) + + self.debug("Command:show inat") + res = ssh_client.execute("show inat") + + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count("USIP: ON"), + 2, + "User source IP should be enabled for INAT service" + ) + + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (self.services["netscaler"]["ipaddress"], e)) + return + + def test_02_acquire_ip_enable_static_nat(self): + """Test associate new IP and enable static NAT for new IP and the VM + """ + + # Validate the following + # 1. user_ip_address.is_system = 0 & user_ip_address.one_to_one_nat=1 + # 2. releases default EIP whose user_ip_address.is_system=1 + # 3. After allowing ingress rule based on source CIDR to the + # respective port, verify that you are able to reach guest with EIP + # 4. check configuration changes for EIP reflects on NS + # commands to verify on NS : + # * "show ip" + # * "show inat" - make sure that output says USIP : ON + + self.debug("Acquiring new IP for network: %s" % self.guest_network.id) + + public_ip = PublicIPAddress.create( + self.apiclient, + accountid=self.account.account.name, + zoneid=self.zone.id, + domainid=self.account.account.domainid, + services=self.services["virtual_machine"] + ) + self.debug("IP address: %s is acquired by network: %s" % ( + public_ip.ipaddress.ipaddress, + self.guest_network.id)) + self.debug("Enabling static NAT for IP Address: %s" % + public_ip.ipaddress.ipaddress) + + StaticNATRule.enable( + self.apiclient, + ipaddressid=public_ip.ipaddress.id, + virtualmachineid=self.virtual_machine.id + ) + + # Fetch details from user_ip_address table in database + self.debug( + "select is_system, one_to_one_nat from user_ip_address where public_ip_address='%s';" \ + % public_ip.ipaddress.ipaddress) + + qresultset = self.dbclient.execute( + "select is_system, one_to_one_nat from user_ip_address where public_ip_address='%s';" \ + % public_ip.ipaddress.ipaddress + ) + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + 0, + "user_ip_address.is_system value should be 0 for new IP" + ) + + self.assertEqual( + qresult[1], + 1, + "user_ip_address.one_to_one_nat value should be 1 for static NAT" + ) + + self.debug( + "select is_system, one_to_one_nat from user_ip_address where public_ip_address='%s';" \ + % self.source_nat.ipaddress) + + qresultset = self.dbclient.execute( + "select is_system, one_to_one_nat from user_ip_address where public_ip_address='%s';" \ + % self.source_nat.ipaddress + ) + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + 0, + "user_ip_address.is_system value should be 0 old source NAT" + ) + + try: + self.debug("SSH into VM: %s" % public_ip.ipaddress.ipaddress) + ssh = self.virtual_machine.get_ssh_client( + ipaddress=public_ip.ipaddress.ipaddress) + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (public_ip.ipaddress.ipaddress, e) + ) + + self.debug("SSH into netscaler: %s" % + self.services["netscaler"]["ipaddress"]) + try: + ssh_client = remoteSSHClient.remoteSSHClient( + self.services["netscaler"]["ipaddress"], + 22, + self.services["netscaler"]["username"], + self.services["netscaler"]["password"], + ) + self.debug("command: show ip") + res = ssh_client.execute("show ip") + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count(public_ip.ipaddress.ipaddress), + 1, + "One IP from EIP pool should be taken and configured on NS" + ) + + self.debug("Command:show inat") + res = ssh_client.execute("show inat") + + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count("USIP: ON"), + 2, + "User source IP should be enabled for INAT service" + ) + + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (self.services["netscaler"]["ipaddress"], e)) + return + + def test_03_disable_static_nat(self): + """Test disable static NAT and release EIP acquired + """ + + # Validate the following + # 1. Disable static NAT. Disables one-to-one NAT and releases EIP + # whose user_ip_address.is_system=0 + # 2. Gets a new ip from EIP pool whose user_ip_address.is_system=1 + # and user_ip_address.one_to_one_nat=1 + # 3. DisassicateIP should mark this EIP whose is_system=0 as free. + # commands to verify on NS : + # * "show ip" + # * "show inat"-make sure that output says USIP : ON + + self.debug( + "Fetching static NAT for VM: %s" % self.virtual_machine.name) + ip_addrs = PublicIPAddress.list( + self.api_client, + associatednetworkid=self.guest_network.id, + isstaticnat=True, + account=self.account.account.name, + domainid=self.account.account.domainid, + listall=True + ) + self.assertEqual( + isinstance(ip_addrs, list), + True, + "List Public IP address should return valid IP address for network" + ) + static_nat = ip_addrs[0] + self.debug("Static NAT for VM: %s is: %s" % ( + self.virtual_machine.name, + static_nat.ipaddress + )) + + # Fetch details from user_ip_address table in database + self.debug( + "select is_system from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress) + + qresultset = self.dbclient.execute( + "select is_system from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress + ) + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + 0, + "user_ip_address.is_system value should be 0" + ) + + self.debug("Disassociate Static NAT: %s" % static_nat.ipaddress) + cmd = disassociateIpAddress.disassociateIpAddressCmd() + cmd.id = static_nat.id + self.apiclient.disassociateIpAddress(cmd) + + self.debug("Sleeping - after disassociating static NAT") + time.sleep(self.services["sleep"]) + + # Fetch details from user_ip_address table in database + self.debug( + "select state from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress) + + qresultset = self.dbclient.execute( + "select state from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress + ) + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + "Free", + "Ip should be marked as Free after disassociate IP" + ) + + self.debug( + "Fetching static NAT for VM: %s" % self.virtual_machine.name) + ip_addrs = PublicIPAddress.list( + self.api_client, + associatednetworkid=self.guest_network.id, + isstaticnat=True, + account=self.account.account.name, + domainid=self.account.account.domainid, + listall=True + ) + self.assertEqual( + isinstance(ip_addrs, list), + True, + "List Public IP address should return valid IP address for network" + ) + static_nat = ip_addrs[0] + self.debug("Static NAT for VM: %s is: %s" % ( + self.virtual_machine.name, + static_nat.ipaddress + )) + + # Fetch details from user_ip_address table in database + self.debug( + "select is_system, one_to_one_nat from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress) + + qresultset = self.dbclient.execute( + "select is_system, one_to_one_nat from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress + ) + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + 1, + "is_system value should be 1 for automatically assigned IP" + ) + self.assertEqual( + qresult[1], + 1, + "one_to_one_nat value should be 1 for automatically assigned IP" + ) + try: + self.debug("SSH into VM: %s" % static_nat.ipaddress) + ssh = self.virtual_machine.get_ssh_client( + ipaddress=static_nat.ipaddress) + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (static_nat.ipaddress, e)) + + self.debug("SSH into netscaler: %s" % + self.services["netscaler"]["ipaddress"]) + try: + ssh_client = remoteSSHClient.remoteSSHClient( + self.services["netscaler"]["ipaddress"], + 22, + self.services["netscaler"]["username"], + self.services["netscaler"]["password"], + ) + self.debug("command: show ip") + res = ssh_client.execute("show ip") + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count(static_nat.ipaddress), + 1, + "One IP from EIP pool should be taken and configured on NS" + ) + + self.debug("Command:show inat") + res = ssh_client.execute("show inat") + + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count("USIP: ON"), + 2, + "User source IP should be enabled for INAT service" + ) + + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (self.services["netscaler"]["ipaddress"], e)) + return + + def test_04_disable_static_nat_system(self): + """Test disable static NAT with system = True + """ + + # Validate the following + # 1. Try to disassociate/disable static NAT on EIP where is_system=1 + # 2. This operation should fail with proper error message. + + self.debug( + "Fetching static NAT for VM: %s" % self.virtual_machine.name) + ip_addrs = PublicIPAddress.list( + self.api_client, + associatednetworkid=self.guest_network.id, + isstaticnat=True, + account=self.account.account.name, + domainid=self.account.account.domainid, + listall=True + ) + self.assertEqual( + isinstance(ip_addrs, list), + True, + "List Public IP address should return valid IP address for network" + ) + static_nat = ip_addrs[0] + self.debug("Static NAT for VM: %s is: %s" % ( + self.virtual_machine.name, + static_nat.ipaddress + )) + + # Fetch details from user_ip_address table in database + self.debug( + "select is_system from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress) + + qresultset = self.dbclient.execute( + "select is_system from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress + ) + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + 1, + "user_ip_address.is_system value should be 1" + ) + + self.debug("Disassociate Static NAT: %s" % static_nat.ipaddress) + + with self.assertRaises(Exception): + cmd = disassociateIpAddress.disassociateIpAddressCmd() + cmd.id = static_nat.id + apiclient.disassociateIpAddress(cmd) + + self.debug("Disassociate system IP failed") + return + + def test_05_destroy_instance(self): + """Test EIO after destroying instance + """ + + # Validate the following + # 1. Destroy instance. Destroy should result in is_system=0 for EIP + # and EIP should also be marked as free. + # 2. Commands to verify on NS : + # * "show ip" + # * "show inat" - make sure that output says USIP: ON + + self.debug( + "Fetching static NAT for VM: %s" % self.virtual_machine.name) + ip_addrs = PublicIPAddress.list( + self.api_client, + associatednetworkid=self.guest_network.id, + isstaticnat=True, + account=self.account.account.name, + domainid=self.account.account.domainid, + listall=True + ) + self.assertEqual( + isinstance(ip_addrs, list), + True, + "List Public IP address should return valid IP address for network" + ) + static_nat = ip_addrs[0] + self.debug("Static NAT for VM: %s is: %s" % ( + self.virtual_machine.name, + static_nat.ipaddress + )) + + self.debug("Destroying an instance: %s" % self.virtual_machine.name) + self.virtual_machine.delete(self.apiclient) + self.debug("Destroy instance complete!") + + config = list_configurations( + self.apiclient, + name='expunge.delay' + ) + self.assertEqual( + isinstance(config, list), + True, + "Check list configurations response" + ) + exp_delay = config[0] + self.debug("expunge.delay: %s" % exp_delay.value) + + config = list_configurations( + self.apiclient, + name='expunge.interval' + ) + self.assertEqual( + isinstance(config, list), + True, + "Check list configurations response" + ) + exp_interval = config[0] + self.debug("expunge.interval: %s" % exp_interval.value) + + # wait for exp_delay+exp_interval - cleans up VM + total_wait = int(exp_interval.value) + int(exp_delay.value) + time.sleep(total_wait) + + vms = VirtualMachine.list( + self.apiclient, + id=self.virtual_machine.id + ) + self.assertEqual( + vms, + None, + "list VM should not return anything after destroy" + ) + # Fetch details from user_ip_address table in database + self.debug( + "select is_system, state from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress) + + qresultset = self.dbclient.execute( + "select is_system, state from user_ip_address where public_ip_address='%s';" \ + % static_nat.ipaddress + ) + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + 0, + "user_ip_address.is_system value should be 0" + ) + + self.assertEqual( + qresult[1], + "Free", + "IP should be marked as Free after destroying VM" + ) + self.debug("SSH into netscaler: %s" % + self.services["netscaler"]["ipaddress"]) + try: + ssh_client = remoteSSHClient.remoteSSHClient( + self.services["netscaler"]["ipaddress"], + 22, + self.services["netscaler"]["username"], + self.services["netscaler"]["password"], + ) + self.debug("command: show ip") + res = ssh_client.execute("show ip") + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count(static_nat.ipaddress), + 0, + "show ip should return nothing after VM destroy" + ) + + self.debug("Command:show inat") + res = ssh_client.execute("show inat") + + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count(static_nat.ipaddress), + 0, + "show inat should return nothing after VM destroy" + ) + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (self.services["netscaler"]["ipaddress"], e)) + return + + +class TestELB(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.api_client = super(TestELB, cls).getClsTestClient().getApiClient() + cls.services = Services().services + # Get Zone, Domain and templates + cls.domain = get_domain(cls.api_client, cls.services) + cls.zone = get_zone(cls.api_client, cls.services) + cls.template = get_template( + cls.api_client, + cls.zone.id, + cls.services["ostypeid"] + ) + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + cls.services["virtual_machine"]["template"] = cls.template.id + + cls.service_offering = ServiceOffering.create( + cls.api_client, + cls.services["service_offering"] + ) + cls.account = Account.create( + cls.api_client, + cls.services["account"], + admin=True, + domainid=cls.domain.id + ) + # Spawn an instance + cls.vm_1 = VirtualMachine.create( + cls.api_client, + cls.services["virtual_machine"], + accountid=cls.account.account.name, + domainid=cls.account.account.domainid, + serviceofferingid=cls.service_offering.id + ) + cls.vm_2 = VirtualMachine.create( + cls.api_client, + cls.services["virtual_machine"], + accountid=cls.account.account.name, + domainid=cls.account.account.domainid, + serviceofferingid=cls.service_offering.id + ) + networks = Network.list( + cls.api_client, + zoneid=cls.zone.id, + listall=True + ) + if isinstance(networks, list): + # Basic zone has only one network i.e Basic network + cls.guest_network = networks[0] + else: + raise Exception( + "List networks returned empty response for zone: %s" % + cls.zone.id) + cls.lb_rule = LoadBalancerRule.create( + cls.api_client, + cls.services["lbrule"], + accountid=cls.account.account.name, + networkid=cls.guest_network.id, + domainid=cls.account.account.domainid + ) + cls.lb_rule.assign(cls.api_client, [cls.vm_1, cls.vm_2]) + + cls._cleanup = [ + cls.account, + cls.service_offering, + ] + return + + @classmethod + def tearDownClass(cls): + try: + #Cleanup resources used + cleanup_resources(cls.api_client, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + return + + def tearDown(self): + try: + #Clean up, terminate the created network offerings + cleanup_resources(self.apiclient, self.cleanup) + self.dbclient.close() + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def test_01_elb_create(self): + """Test ELB by creating a LB rule + """ + + # Validate the following + # 1. Deploy 2 instances + # 2. Create LB rule to port 22 for the VMs and try to access VMs with + # EIP:port. Make sure that ingress rule is created to allow access + # with universal CIDR (0.0.0.0/0) + # 3. For LB rule IP user_ip_address.is_system=1 + # 4. check configuration changes for EIP reflects on NS + # commands to verify on NS : + # * "show ip" + # * "show lb vserer"-make sure that output says they are all up + # and running and USNIP : ON + + # Verify listSecurity groups response + security_groups = SecurityGroup.list( + self.apiclient, + account=self.account.account.name, + domainid=self.account.account.domainid + ) + self.assertEqual( + isinstance(security_groups, list), + True, + "Check for list security groups response" + ) + self.assertEqual( + len(security_groups), + 1, + "Check List Security groups response" + ) + self.debug("List Security groups response: %s" % + str(security_groups)) + + security_group = security_groups[0] + + self.debug( + "Creating Ingress rule to allow SSH on default security group") + + cmd = authorizeSecurityGroupIngress.authorizeSecurityGroupIngressCmd() + cmd.domainid = self.account.account.domainid + cmd.account = self.account.account.name + cmd.securitygroupid = security_group.id + cmd.protocol = 'TCP' + cmd.startport = 22 + cmd.endport = 22 + cmd.cidrlist = '0.0.0.0/0' + self.apiclient.authorizeSecurityGroupIngress(cmd) + + self.debug( + "Fetching LB IP for account: %s" % self.account.account.name) + ip_addrs = PublicIPAddress.list( + self.api_client, + associatednetworkid=self.guest_network.id, + account=self.account.account.name, + domainid=self.account.account.domainid, + forloadbalancing=True, + listall=True + ) + self.assertEqual( + isinstance(ip_addrs, list), + True, + "List Public IP address should return valid IP address for network" + ) + + lb_ip = ip_addrs[0] + self.debug("LB IP generated for account: %s is: %s" % ( + self.account.account.name, + lb_ip.ipaddress + )) + self.debug("SSHing into VMs using ELB IP: %s" % lb_ip.ipaddress) + try: + ssh_1 = self.vm_1.get_ssh_client(ipaddress=lb_ip.ipaddress) + self.debug("Command: hostname") + result = ssh_1.execute("hostname") + self.debug("Result: %s" % result) + + if isinstance(result, list): + res = result[0] + else: + self.fail("hostname retrieval failed!") + + self.assertIn( + res, + [self.vm_1.name, self.vm_2.name], + "SSH should return hostname of one of the VM" + ) + + ssh_2 = self.vm_2.get_ssh_client(ipaddress=lb_ip.ipaddress) + self.debug("Command: hostname") + result = ssh_2.execute("hostname") + self.debug("Result: %s" % result) + + if isinstance(result, list): + res = result[0] + else: + self.fail("hostname retrieval failed!") + self.assertIn( + res, + [self.vm_1.name, self.vm_2.name], + "SSH should return hostname of one of the VM" + ) + except Exception as e: + self.fail( + "SSH Access failed for %s: %s" % (self.vm_1.ipaddress, e)) + + # Fetch details from user_ip_address table in database + self.debug( + "select is_system from user_ip_address where public_ip_address='%s';" \ + % lb_ip.ipaddress) + + qresultset = self.dbclient.execute( + "select is_system from user_ip_address where public_ip_address='%s';" \ + % lb_ip.ipaddress + ) + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + 1, + "is_system value should be 1 for system generated LB rule" + ) + self.debug("SSH into netscaler: %s" % + self.services["netscaler"]["ipaddress"]) + try: + ssh_client = remoteSSHClient.remoteSSHClient( + self.services["netscaler"]["ipaddress"], + 22, + self.services["netscaler"]["username"], + self.services["netscaler"]["password"], + ) + self.debug("command: show ip") + res = ssh_client.execute("show ip") + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count(lb_ip.ipaddress), + 1, + "One IP from EIP pool should be taken and configured on NS" + ) + + self.debug("Command:show lb vserver") + res = ssh_client.execute("show lb vserver") + + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count("State: UP"), + 2, + "User subnet IP should be enabled for LB service" + ) + + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (self.services["netscaler"]["ipaddress"], e)) + return + + def test_02_elb_acquire_and_create(self): + """Test ELB by acquiring IP and then creating a LB rule + """ + + # Validate the following + # 1. Deploy 2 instances + # 2. Create LB rule to port 22 for the VMs and try to access VMs with + # EIP:port. Make sure that ingress rule is created to allow access + # with universal CIDR (0.0.0.0/0) + # 3. For LB rule IP user_ip_address.is_system=0 + # 4. check configuration changes for EIP reflects on NS + # commands to verify on NS : + # * "show ip" + # * "show lb vserer" - make sure that output says they are all up + # and running and USNIP : ON + + self.debug("Acquiring new IP for network: %s" % self.guest_network.id) + + public_ip = PublicIPAddress.create( + self.apiclient, + accountid=self.account.account.name, + zoneid=self.zone.id, + domainid=self.account.account.domainid, + services=self.services["virtual_machine"] + ) + self.debug("IP address: %s is acquired by network: %s" % ( + public_ip.ipaddress.ipaddress, + self.guest_network.id)) + + self.debug("Creating LB rule for public IP: %s" % + public_ip.ipaddress.ipaddress) + lb_rule = LoadBalancerRule.create( + self.apiclient, + self.services["lbrule"], + accountid=self.account.account.name, + ipaddressid=public_ip.ipaddress.id, + networkid=self.guest_network.id, + domainid=self.account.account.domaind + ) + self.debug("Assigning VMs (%s, %s) to LB rule: %s" % (self.vm_1.name, + self.vm_2.name, + lb_rule.name)) + lb_rule.assign(self.apiclient, [self.vm_1, self.vm_2]) + + self.debug("SSHing into VMs using ELB IP: %s" % + public_ip.ipaddress.ipaddress) + try: + ssh_1 = self.vm_1.get_ssh_client( + ipaddress=public_ip.ipaddress.ipaddress) + self.debug("Command: hostname") + result = ssh_1.execute("hostname") + self.debug("Result: %s" % result) + + if isinstance(result, list): + res = result[0] + else: + self.fail("hostname retrieval failed!") + self.assertIn( + res, + [self.vm_1.name, self.vm_2.name], + "SSH should return hostname of one of the VM" + ) + + ssh_2 = self.vm_2.get_ssh_client( + ipaddress=public_ip.ipaddress.ipaddress) + self.debug("Command: hostname") + result = ssh_2.execute("hostname") + self.debug("Result: %s" % result) + + if isinstance(result, list): + res = result[0] + else: + self.fail("hostname retrieval failed!") + self.assertIn( + res, + [self.vm_1.name, self.vm_2.name], + "SSH should return hostname of one of the VM" + ) + except Exception as e: + self.fail( + "SSH Access failed for %s: %s" % (self.vm_1.ipaddress, e)) + + # Fetch details from user_ip_address table in database + self.debug( + "select is_system from user_ip_address where public_ip_address='%s';" \ + % public_ip.ipaddress.ipaddress) + + qresultset = self.dbclient.execute( + "select is_system from user_ip_address where public_ip_address='%s';" \ + % public_ip.ipaddress.ipaddress) + + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "Check DB Query result set" + ) + qresult = qresultset[0] + + self.assertEqual( + qresult[0], + 0, + "is_system value should be 0 for non-system generated LB rule" + ) + self.debug("SSH into netscaler: %s" % + self.services["netscaler"]["ipaddress"]) + try: + ssh_client = remoteSSHClient.remoteSSHClient( + self.services["netscaler"]["ipaddress"], + 22, + self.services["netscaler"]["username"], + self.services["netscaler"]["password"], + ) + self.debug("command: show ip") + res = ssh_client.execute("show ip") + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count(public_ip.ipaddress.ipaddress), + 1, + "One IP from EIP pool should be taken and configured on NS" + ) + + self.debug("Command:show lb vserver") + res = ssh_client.execute("show lb vserver") + + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count("State: UP"), + 4, + "User subnet IP should be enabled for LB service" + ) + + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (self.services["netscaler"]["ipaddress"], e)) + return + + def test_03_elb_delete_lb_system(self): + """Test delete LB rule generated with public IP with is_system = 1 + """ + + # Validate the following + # 1. Deleting LB rule should release EIP where is_system=1 + # 2. check configuration changes for EIP reflects on NS + # commands to verify on NS: + # * "show ip" + # * "show lb vserer"-make sure that output says they are all up and + # running and USNIP : ON + + self.debug( + "Fetching LB IP for account: %s" % self.account.account.name) + ip_addrs = PublicIPAddress.list( + self.api_client, + associatednetworkid=self.guest_network.id, + account=self.account.account.name, + domainid=self.account.account.domainid, + forloadbalancing=True, + listall=True + ) + self.assertEqual( + isinstance(ip_addrs, list), + True, + "List Public IP address should return valid IP address for network" + ) + + lb_ip = ip_addrs[0] + self.debug("LB IP generated for account: %s is: %s" % ( + self.account.account.name, + lb_ip.ipaddress + )) + + self.debug("Deleting LB rule: %s" % self.lb_rule.id) + self.lb_rule.delete(self.apiclient) + + config = list_configurations( + self.apiclient, + name='network.gc.wait' + ) + self.assertEqual( + isinstance(config, list), + True, + "Check list configurations response" + ) + gc_delay = config[0] + self.debug("network.gc.wait: %s" % gc_delay.value) + + config = list_configurations( + self.apiclient, + name='network.gc.interval' + ) + self.assertEqual( + isinstance(config, list), + True, + "Check list configurations response" + ) + gc_interval = config[0] + self.debug("network.gc.intervall: %s" % gc_interval.value) + + # wait for exp_delay+exp_interval - cleans up VM + total_wait = int(gc_interval.value) + int(gc_delay.value) + time.sleep(total_wait) + + self.debug("SSH into netscaler: %s" % + self.services["netscaler"]["ipaddress"]) + try: + ssh_client = remoteSSHClient.remoteSSHClient( + self.services["netscaler"]["ipaddress"], + 22, + self.services["netscaler"]["username"], + self.services["netscaler"]["password"], + ) + self.debug("command: show ip") + res = ssh_client.execute("show ip") + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count(lb_ip.ipaddress), + 1, + "One IP from EIP pool should be taken and configured on NS" + ) + + self.debug("Command:show lb vserver") + res = ssh_client.execute("show lb vserver") + + result = str(res) + self.debug("Output: %s" % result) + + self.assertEqual( + result.count("State: UP"), + 2, + "User subnet IP should be enabled for LB service" + ) + + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (self.services["netscaler"]["ipaddress"], e)) + return + + def test_04_delete_lb_on_eip(self): + """Test delete LB rule generated on EIP + """ + + # Validate the following + # 1. Deleting LB rule won't release EIP where is_system=0 + # 2. disassociateIP must release the above IP + # 3. check configuration changes for EIP reflects on NS + # commands to verify on NS : + # * "show ip" + # * "show lb vserer"-make sure that output says they are all up and + # running and USNIP : ON + + # Fetch details from account_id table in database + self.debug( + "select id from account where account_name='%s';" \ + % self.account.account.name) + + qresultset = self.dbclient.execute( + "select id from account where account_name='%s';" \ + % self.account.account.name) + + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "DB query should return a valid public IP address" + ) + qresult = qresultset[0] + account_id = qresult[0] + # Fetch details from user_ip_address table in database + self.debug( + "select public_ip_address from user_ip_address where is_system=0 and account_id=%s;" \ + % account_id) + + qresultset = self.dbclient.execute( + "select public_ip_address from user_ip_address where is_system=0 and account_id=%s;" \ + % account_id) + + self.assertEqual( + isinstance(qresultset, list), + True, + "Check DB query result set for valid data" + ) + + self.assertNotEqual( + len(qresultset), + 0, + "DB query should return a valid public IP address" + ) + qresult = qresultset[0] + public_ip = qresult[0] + + self.debug( + "Fetching public IP for account: %s" % self.account.account.name) + ip_addrs = PublicIPAddress.list( + self.api_client, + ipaddress=public_ip, + listall=True + ) + self.assertEqual( + isinstance(ip_addrs, list), + True, + "List Public IP address should return valid IP address for network" + ) + + lb_ip = ip_addrs[0] + + lb_rules = LoadBalancerRule.list( + self.apiclient, + publicipid=lb_ip.id, + listall=True + ) + self.assertEqual( + isinstance(lb_rules, list), + True, + "Atleast one LB rule must be present for public IP address" + ) + lb_rule = lb_rules[0] + self.debug("Deleting LB rule associated with IP: %s" % public_ip) + + try: + cmd = deleteLoadBalancerRule.deleteLoadBalancerRuleCmd() + cmd.id = lb_rule.id + self.apiclient.deleteLoadBalancerRule(cmd) + except Exception as e: + self.fail("Deleting LB rule failed for IP: %s-%s" % (public_ip, e)) + + config = list_configurations( + self.apiclient, + name='network.gc.wait' + ) + self.assertEqual( + isinstance(config, list), + True, + "Check list configurations response" + ) + gc_delay = config[0] + self.debug("network.gc.wait: %s" % gc_delay.value) + + config = list_configurations( + self.apiclient, + name='network.gc.interval' + ) + self.assertEqual( + isinstance(config, list), + True, + "Check list configurations response" + ) + gc_interval = config[0] + self.debug("network.gc.intervall: %s" % gc_interval.value) + + # wait for exp_delay+exp_interval - cleans up VM + total_wait = int(gc_interval.value) + int(gc_delay.value) + time.sleep(total_wait) + + self.debug("LB rule deleted!") + + ip_addrs = PublicIPAddress.list( + self.api_client, + ipaddress=public_ip, + listall=True + ) + self.assertEqual( + isinstance(ip_addrs, list), + True, + "Deleting LB rule should not delete public IP" + ) + self.debug("SSH into netscaler: %s" % + self.services["netscaler"]["ipaddress"]) + try: + ssh_client = remoteSSHClient.remoteSSHClient( + self.services["netscaler"]["ipaddress"], + 22, + self.services["netscaler"]["username"], + self.services["netscaler"]["password"], + ) + self.debug("command: show ip") + res = ssh_client.execute("show ip") + result = str(res) + self.debug("Output: %s" % result) + + self.assertNotEqual( + result.count(public_ip), + 1, + "One IP from EIP pool should be taken and configured on NS" + ) + + self.debug("Command:show lb vserver") + res = ssh_client.execute("show lb vserver") + + result = str(res) + self.debug("Output: %s" % result) + + self.assertNotEqual( + result.count("State: UP"), + 2, + "User subnet IP should be enabled for LB service" + ) + + except Exception as e: + self.fail("SSH Access failed for %s: %s" % \ + (self.services["netscaler"]["ipaddress"], e)) + return diff --git a/test/integration/component/test_high_availability.py b/test/integration/component/test_high_availability.py index b4ccbafc41a..9e95df1ffc8 100644 --- a/test/integration/component/test_high_availability.py +++ b/test/integration/component/test_high_availability.py @@ -77,7 +77,19 @@ class Services: "publicport": 22, "protocol": 'TCP', }, - "ostypeid": '9958b10f-9e5d-4ef1-908d-a047372d823b', + "templates": { + "displaytext": "Public Template", + "name": "Public template", + "ostypeid": '946b031b-0e10-4f4a-a3fc-d212ae2ea07f', + "url": "http://download.cloud.com/releases/2.0.0/UbuntuServer-10-04-64bit.vhd.bz2", + "hypervisor": 'XenServer', + "format" : 'VHD', + "isfeatured": True, + "ispublic": True, + "isextractable": True, + "templatefilter": 'self', + }, + "ostypeid": '946b031b-0e10-4f4a-a3fc-d212ae2ea07f', # Cent OS 5.3 (64 bit) "sleep": 60, "timeout": 100, @@ -127,14 +139,14 @@ class TestHighAvailability(cloudstackTestCase): ] return -# @classmethod -# def tearDownClass(cls): -# try: -# #Cleanup resources used -# cleanup_resources(cls.api_client, cls._cleanup) -# except Exception as e: -# raise Exception("Warning: Exception during cleanup : %s" % e) -# return + @classmethod + def tearDownClass(cls): + try: + #Cleanup resources used + cleanup_resources(cls.api_client, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return def setUp(self): self.apiclient = self.testClient.getApiClient() @@ -148,15 +160,15 @@ class TestHighAvailability(cloudstackTestCase): self.cleanup = [self.account] return -# def tearDown(self): -# try: -# #Clean up, terminate the created accounts, domains etc -# cleanup_resources(self.apiclient, self.cleanup) -# self.testClient.close() -# except Exception as e: -# raise Exception("Warning: Exception during cleanup : %s" % e) -# return - + def tearDown(self): + try: + #Clean up, terminate the created accounts, domains etc + cleanup_resources(self.apiclient, self.cleanup) + self.testClient.close() + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + @unittest.skip("skipped") def test_01_host_maintenance_mode(self): """Test host maintenance mode """ @@ -255,7 +267,7 @@ class TestHighAvailability(cloudstackTestCase): )) self.debug("Creating PF rule for IP address: %s" % public_ip.ipaddress.ipaddress) - nat_rule= NATRule.create( + nat_rule = NATRule.create( self.apiclient, virtual_machine, self.services["natrule"], @@ -265,6 +277,15 @@ class TestHighAvailability(cloudstackTestCase): self.debug("Creating LB rule on IP with NAT: %s" % public_ip.ipaddress.ipaddress) + # Open up firewall port for SSH + fw_rule = FireWallRule.create( + self.apiclient, + ipaddressid=public_ip.ipaddress.id, + protocol=self.services["natrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["natrule"]["publicport"], + endport=self.services["natrule"]["publicport"] + ) # Create Load Balancer rule on IP already having NAT rule lb_rule = LoadBalancerRule.create( self.apiclient, @@ -318,7 +339,11 @@ class TestHighAvailability(cloudstackTestCase): vm = vms[0] self.debug("VM 1 state: %s" % vm.state) - if vm.state in ["Stopping", "Stopped", "Running", "Starting"]: + if vm.state in ["Stopping", + "Stopped", + "Running", + "Starting", + "Migrating"]: if vm.state == "Running": break else: @@ -418,7 +443,53 @@ class TestHighAvailability(cloudstackTestCase): self.debug( "VM state after enabling maintenance on first host: %s" % vm.state) - if vm.state in ["Stopping", "Stopped", "Running", "Starting"]: + if vm.state in [ + "Stopping", + "Stopped", + "Running", + "Starting", + "Migrating" + ]: + if vm.state == "Running": + break + else: + time.sleep(self.services["sleep"]) + timeout = timeout - 1 + else: + self.fail( + "VM migration from one-host-to-other failed while enabling maintenance" + ) + + # Poll and check the status of VMs + timeout = self.services["timeout"] + while True: + vms = VirtualMachine.list( + self.apiclient, + account=self.account.account.name, + domainid=self.account.account.domainid, + listall=True + ) + self.assertEqual( + isinstance(vms, list), + True, + "List VMs should return valid response for deployed VM" + ) + self.assertNotEqual( + len(vms), + 0, + "List VMs should return valid response for deployed VM" + ) + vm = vms[1] + self.debug( + "VM state after enabling maintenance on first host: %s" % + vm.state) + if vm.state in [ + "Stopping", + "Stopped", + "Running", + "Starting", + "Migrating" + ]: if vm.state == "Running": break else: @@ -438,6 +509,7 @@ class TestHighAvailability(cloudstackTestCase): "Running", "Deployed VM should be in Running state" ) + # Spawn an instance on other host virtual_machine_3 = VirtualMachine.create( self.apiclient, @@ -486,6 +558,7 @@ class TestHighAvailability(cloudstackTestCase): cmd.id = second_host self.apiclient.cancelHostMaintenance(cmd) self.debug("Maintenance mode canceled for host: %s" % second_host) + self.debug("Waiting for SSVMs to come up") wait_for_ssvms( self.apiclient, @@ -596,13 +669,23 @@ class TestHighAvailability(cloudstackTestCase): )) self.debug("Creating PF rule for IP address: %s" % public_ip.ipaddress.ipaddress) - nat_rule= NATRule.create( + nat_rule = NATRule.create( self.apiclient, virtual_machine, self.services["natrule"], ipaddressid=public_ip.ipaddress.id ) + # Open up firewall port for SSH + fw_rule = FireWallRule.create( + self.apiclient, + ipaddressid=public_ip.ipaddress.id, + protocol=self.services["natrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["natrule"]["publicport"], + endport=self.services["natrule"]["publicport"] + ) + self.debug("Creating LB rule on IP with NAT: %s" % public_ip.ipaddress.ipaddress) @@ -624,7 +707,7 @@ class TestHighAvailability(cloudstackTestCase): self.fail("SSH Access failed for %s: %s" % \ (virtual_machine.ipaddress, e) ) - # Get the Root disk of VM + # Get the Root disk of VM volumes = list_volumes( self.apiclient, virtualmachineid=virtual_machine.id, @@ -662,6 +745,7 @@ class TestHighAvailability(cloudstackTestCase): snapshot.id, "Check snapshot id in list resources call" ) + # Generate template from the snapshot self.debug("Generating template from snapshot: %s" % snapshot.name) template = Template.create_from_snapshot( @@ -669,7 +753,6 @@ class TestHighAvailability(cloudstackTestCase): snapshot, self.services["templates"] ) - self.cleanup.append(template) self.debug("Created template from snapshot: %s" % template.id) templates = list_templates( @@ -725,7 +808,11 @@ class TestHighAvailability(cloudstackTestCase): vm = vms[0] self.debug("VM 1 state: %s" % vm.state) - if vm.state in ["Stopping", "Stopped", "Running", "Starting"]: + if vm.state in ["Stopping", + "Stopped", + "Running", + "Starting", + "Migrating"]: if vm.state == "Running": break else: @@ -789,7 +876,7 @@ class TestHighAvailability(cloudstackTestCase): self.apiclient.cancelHostMaintenance(cmd) self.debug("Maintenance mode canceled for host: %s" % first_host) - # Get the Root disk of VM + # Get the Root disk of VM volumes = list_volumes( self.apiclient, virtualmachineid=virtual_machine_2.id, @@ -827,6 +914,7 @@ class TestHighAvailability(cloudstackTestCase): snapshot.id, "Check snapshot id in list resources call" ) + # Generate template from the snapshot self.debug("Generating template from snapshot: %s" % snapshot.name) template = Template.create_from_snapshot( @@ -834,7 +922,6 @@ class TestHighAvailability(cloudstackTestCase): snapshot, self.services["templates"] ) - self.cleanup.append(template) self.debug("Created template from snapshot: %s" % template.id) templates = list_templates( @@ -892,7 +979,49 @@ class TestHighAvailability(cloudstackTestCase): self.debug( "VM state after enabling maintenance on first host: %s" % vm.state) - if vm.state in ["Stopping", "Stopped", "Running", "Starting"]: + if vm.state in ["Stopping", + "Stopped", + "Running", + "Starting", + "Migrating"]: + if vm.state == "Running": + break + else: + time.sleep(self.services["sleep"]) + timeout = timeout - 1 + else: + self.fail( + "VM migration from one-host-to-other failed while enabling maintenance" + ) + + # Poll and check the status of VMs + timeout = self.services["timeout"] + while True: + vms = VirtualMachine.list( + self.apiclient, + account=self.account.account.name, + domainid=self.account.account.domainid, + listall=True + ) + self.assertEqual( + isinstance(vms, list), + True, + "List VMs should return valid response for deployed VM" + ) + self.assertNotEqual( + len(vms), + 0, + "List VMs should return valid response for deployed VM" + ) + vm = vms[1] + self.debug( + "VM state after enabling maintenance on first host: %s" % + vm.state) + if vm.state in ["Stopping", + "Stopped", + "Running", + "Starting", + "Migrating"]: if vm.state == "Running": break else: @@ -912,6 +1041,7 @@ class TestHighAvailability(cloudstackTestCase): "Running", "Deployed VM should be in Running state" ) + # Spawn an instance on other host virtual_machine_3 = VirtualMachine.create( self.apiclient, @@ -945,21 +1075,12 @@ class TestHighAvailability(cloudstackTestCase): "Deployed VM should be in Running state" ) - # Should be able to SSH VM - try: - self.debug("SSH into VM: %s" % virtual_machine.id) - ssh = virtual_machine.get_ssh_client( - ipaddress=public_ip.ipaddress.ipaddress) - except Exception as e: - self.fail("SSH Access failed for %s: %s" % \ - (virtual_machine.ipaddress, e) - ) - self.debug("Canceling host maintenance for ID: %s" % second_host) cmd = cancelHostMaintenance.cancelHostMaintenanceCmd() cmd.id = second_host self.apiclient.cancelHostMaintenance(cmd) self.debug("Maintenance mode canceled for host: %s" % second_host) + self.debug("Waiting for SSVMs to come up") wait_for_ssvms( self.apiclient, diff --git a/test/integration/component/test_network_offering.py b/test/integration/component/test_network_offering.py index b536da11a2d..ddacad1f4c5 100644 --- a/test/integration/component/test_network_offering.py +++ b/test/integration/component/test_network_offering.py @@ -44,7 +44,7 @@ class Services: "name": "Tiny Instance", "displaytext": "Tiny Instance", "cpunumber": 1, - "cpuspeed": 100, # in MHz + "cpuspeed": 100, # in MHz "memory": 64, # In MBs }, "network_offering": { @@ -54,16 +54,16 @@ class Services: "supportedservices": 'Dhcp,Dns,SourceNat,PortForwarding,Vpn,Firewall,Lb,UserData,StaticNat', "traffictype": 'GUEST', "availability": 'Optional', - "serviceProviderList": { + "serviceProviderList" : { "Dhcp": 'VirtualRouter', "Dns": 'VirtualRouter', "SourceNat": 'VirtualRouter', "PortForwarding": 'VirtualRouter', - "Vpn": 'VirtualRouter', - "Firewall": 'VirtualRouter', - "Lb": 'VirtualRouter', - "UserData": 'VirtualRouter', - "StaticNat": 'VirtualRouter', + "Vpn": 'VirtualRouter', + "Firewall": 'VirtualRouter', + "Lb": 'VirtualRouter', + "UserData": 'VirtualRouter', + "StaticNat": 'VirtualRouter', }, }, "network_offering_netscaler": { @@ -73,7 +73,7 @@ class Services: "supportedservices": 'Dhcp,Dns,SourceNat,PortForwarding,Vpn,Firewall,Lb,UserData,StaticNat', "traffictype": 'GUEST', "availability": 'Optional', - "serviceProviderList": { + "serviceProviderList" : { "Dhcp": 'VirtualRouter', "Dns": 'VirtualRouter', "SourceNat": 'VirtualRouter', @@ -115,7 +115,7 @@ class Services: "publicport": 66, "protocol": "TCP" }, - "fw_rule": { + "fw_rule":{ "startport": 1, "endport": 6000, "cidr": '55.55.0.0/11', @@ -137,7 +137,7 @@ class Services: # Cent OS 5.3 (64 bit) "sleep": 60, "timeout": 10, - "mode": 'advanced' + "mode":'advanced' } @@ -240,7 +240,7 @@ class TestNOVirtualRouter(cloudstackTestCase): self.debug("Created n/w offering with ID: %s" % self.network_offering.id) - # Enable Network offering + # Enable Network offering self.network_offering.update(self.apiclient, state='Enabled') # Creating network using the network offering created @@ -252,7 +252,7 @@ class TestNOVirtualRouter(cloudstackTestCase): accountid=self.account.account.name, domainid=self.account.account.domainid, networkofferingid=self.network_offering.id, - zoneid=self.zone.id + zoneid=self.zone.id ) self.debug("Created network with ID: %s" % self.network.id) @@ -268,6 +268,7 @@ class TestNOVirtualRouter(cloudstackTestCase): networkids=[str(self.network.id)] ) self.debug("Deployed VM in network: %s" % self.network.id) + src_nat_list = PublicIPAddress.list( self.apiclient, associatednetworkid=self.network.id, @@ -286,7 +287,9 @@ class TestNOVirtualRouter(cloudstackTestCase): 0, "Length of response from listPublicIp should not be 0" ) + src_nat = src_nat_list[0] + self.debug("Trying to create LB rule on source NAT IP: %s" % src_nat.ipaddress) # Create Load Balancer rule with source NAT @@ -326,7 +329,7 @@ class TestNOVirtualRouter(cloudstackTestCase): ip_with_nat_rule.ipaddress.ipaddress) NATRule.create( self.apiclient, - virtual_machine, + virtual_machine, self.services["natrule"], ipaddressid=ip_with_nat_rule.ipaddress.id ) @@ -478,7 +481,7 @@ class TestNOVirtualRouter(cloudstackTestCase): self.debug("Created n/w offering with ID: %s" % self.network_offering.id) - # Enable Network offering + # Enable Network offering self.network_offering.update(self.apiclient, state='Enabled') # Creating network using the network offering created @@ -490,7 +493,7 @@ class TestNOVirtualRouter(cloudstackTestCase): accountid=self.account.account.name, domainid=self.account.account.domainid, networkofferingid=self.network_offering.id, - zoneid=self.zone.id + zoneid=self.zone.id ) self.debug("Created network with ID: %s" % self.network.id) @@ -506,6 +509,7 @@ class TestNOVirtualRouter(cloudstackTestCase): networkids=[str(self.network.id)] ) self.debug("Deployed VM in network: %s" % self.network.id) + src_nat_list = PublicIPAddress.list( self.apiclient, associatednetworkid=self.network.id, @@ -694,7 +698,7 @@ class TestNOVirtualRouter(cloudstackTestCase): vpns = Vpn.list( self.apiclient, publicipid=src_nat.id, - listall=True, + listall=True, ) self.assertEqual( @@ -789,13 +793,14 @@ class TestNOWithNetscaler(cloudstackTestCase): # 5. On an ipaddress that has Lb rules , we should NOT allow firewall # rules to be programmed. # 6. On an ipaddress that has Lb rules , we should NOT allow PF rules - # to be programmed. + # to be programmed. # 7. We should be allowed to program multiple PF rules on the same Ip # address on different public ports. # 8. We should be allowed to program multiple LB rules on the same Ip - # address for different public port ranges. + # address for different public port ranges. # 9. On source NAT ipaddress, we should NOT be allowed to Enable VPN. + # Create a network offering with all virtual router services enabled self.debug( "Creating n/w offering with all services in VR & conserve mode:ON" @@ -837,6 +842,7 @@ class TestNOWithNetscaler(cloudstackTestCase): networkids=[str(self.network.id)] ) self.debug("Deployed VM in network: %s" % self.network.id) + src_nat_list = PublicIPAddress.list( self.apiclient, associatednetworkid=self.network.id, @@ -882,7 +888,7 @@ class TestNOWithNetscaler(cloudstackTestCase): ) self.debug("Creating firewall rule on source NAT: %s" % src_nat.ipaddress) - #Create Firewall rule on source NAT + #Create Firewall rule on source NAT fw_rule = FireWallRule.create( self.apiclient, ipaddressid=src_nat.id, @@ -1056,13 +1062,14 @@ class TestNOWithNetscaler(cloudstackTestCase): # 5. On an ipaddress that has Lb rules , we should NOT allow firewall # rules to be programmed. # 6. On an ipaddress that has Lb rules , we should NOT allow PF rules - # to be programmed. + # to be programmed. # 7. We should be allowed to program multiple PF rules on the same Ip # address on different public ports. # 8. We should be allowed to program multiple LB rules on the same Ip - # address for different public port ranges. + # address for different public port ranges. # 9. On source NAT ipaddress, we should be allowed to Enable VPN. + # Create a network offering with all virtual router services enabled self.debug( "Creating n/w offering with all services in VR & conserve mode:ON" @@ -1104,6 +1111,7 @@ class TestNOWithNetscaler(cloudstackTestCase): networkids=[str(self.network.id)] ) self.debug("Deployed VM in network: %s" % self.network.id) + src_nat_list = PublicIPAddress.list( self.apiclient, associatednetworkid=self.network.id, @@ -1452,6 +1460,7 @@ class TestNetworkUpgrade(cloudstackTestCase): networkids=[str(self.network.id)] ) self.debug("Deployed VM in network: %s" % self.network.id) + src_nat_list = PublicIPAddress.list( self.apiclient, associatednetworkid=self.network.id, @@ -1535,6 +1544,7 @@ class TestNetworkUpgrade(cloudstackTestCase): ) self.cleanup.append(ns_lb_offering) ns_lb_offering.update(self.apiclient, state='Enabled') + #Stop all the VMs associated with network to update cidr self.debug("Stopping the VM: %s" % virtual_machine.name) virtual_machine.stop(self.apiclient) @@ -1549,6 +1559,7 @@ class TestNetworkUpgrade(cloudstackTestCase): ) self.debug("Network upgrade failed!") + self.debug("Deleting LB Rule: %s" % lb_rule.id) lb_rule.delete(self.apiclient) self.debug("LB rule deleted") @@ -1646,6 +1657,7 @@ class TestNetworkUpgrade(cloudstackTestCase): networkids=[str(self.network.id)] ) self.debug("Deployed VM in network: %s" % self.network.id) + src_nat_list = PublicIPAddress.list( self.apiclient, associatednetworkid=self.network.id, @@ -1729,6 +1741,7 @@ class TestNetworkUpgrade(cloudstackTestCase): ) self.cleanup.append(ns_lb_offering) ns_lb_offering.update(self.apiclient, state='Enabled') + #Stop all the VMs associated with network to update cidr self.debug("Stopping the VM: %s" % virtual_machine.name) virtual_machine.stop(self.apiclient) @@ -1743,6 +1756,7 @@ class TestNetworkUpgrade(cloudstackTestCase): ) self.debug("Network upgrade failed!") + self.debug("Deleting LB Rule: %s" % lb_rule.id) lb_rule.delete(self.apiclient) self.debug("LB rule deleted") diff --git a/test/integration/component/test_project_resources.py b/test/integration/component/test_project_resources.py index 624b9846a53..a98f860a67b 100644 --- a/test/integration/component/test_project_resources.py +++ b/test/integration/component/test_project_resources.py @@ -20,6 +20,7 @@ from marvin.cloudstackAPI import * from integration.lib.utils import * from integration.lib.base import * from integration.lib.common import * +from marvin.remoteSSHClient import remoteSSHClient import datetime class Services: @@ -1311,4 +1312,4 @@ class TestSecurityGroup(cloudstackTestCase): domainid=self.account.account.domainid, securitygroupids=[security_group.id], ) - return \ No newline at end of file + return diff --git a/test/integration/component/test_projects.py b/test/integration/component/test_projects.py index 8dd64ee8a46..fd195436905 100644 --- a/test/integration/component/test_projects.py +++ b/test/integration/component/test_projects.py @@ -1806,4 +1806,4 @@ class TestProjectSuspendActivate(cloudstackTestCase): 'Running', "VM should be in Running state after project activation" ) - return \ No newline at end of file + return diff --git a/test/integration/component/test_resource_limits.py b/test/integration/component/test_resource_limits.py index 9dea37f2ac0..c7a0c6c8c5b 100644 --- a/test/integration/component/test_resource_limits.py +++ b/test/integration/component/test_resource_limits.py @@ -15,6 +15,7 @@ """ #Import Local Modules import marvin +from marvin.cloudstackTestCase import * from marvin.cloudstackAPI import * from integration.lib.utils import * from integration.lib.base import * diff --git a/test/integration/component/test_routers.py b/test/integration/component/test_routers.py index 05792336d69..a1bdb166ee8 100644 --- a/test/integration/component/test_routers.py +++ b/test/integration/component/test_routers.py @@ -17,10 +17,10 @@ import marvin from marvin.cloudstackTestCase import * from marvin.cloudstackAPI import * -from marvin.remoteSSHClient import remoteSSHClient from integration.lib.utils import * from integration.lib.base import * from integration.lib.common import * +from marvin.remoteSSHClient import remoteSSHClient #Import System modules import time @@ -219,7 +219,7 @@ class TestRouterServices(cloudstackTestCase): True, "Check for list networks response return valid data" ) - + self.assertNotEqual( len(networks), 0, @@ -228,7 +228,7 @@ class TestRouterServices(cloudstackTestCase): for network in networks: self.assertIn( network.state, - ['Implemented','Allocated'], + ['Implemented', 'Allocated'], "Check list network response for network state" ) self.debug("Network ID: %s & Network state: %s" % ( @@ -371,7 +371,7 @@ class TestRouterServices(cloudstackTestCase): for network in networks: self.assertIn( network.state, - ['Implemented','Allocated'], + ['Implemented', 'Allocated'], "Check list network response for network state" ) self.debug("Network ID: %s & Network state: %s" % ( @@ -486,7 +486,7 @@ class TestRouterServices(cloudstackTestCase): serviceofferingid=self.service_offering.id ) self.debug("Deployed a VM with ID: %s" % vm.id) - + virtual_machines = list_virtual_machines( self.apiclient, id=vm.id, @@ -499,7 +499,7 @@ class TestRouterServices(cloudstackTestCase): True, "Check for list virtual machines response return valid data" ) - + self.assertNotEqual( len(virtual_machines), 0, @@ -524,7 +524,7 @@ class TestRouterServices(cloudstackTestCase): True, "Check for list routers response return valid data" ) - + self.assertNotEqual( len(routers), 0, @@ -556,7 +556,7 @@ class TestRouterServices(cloudstackTestCase): True, "Check for list VMs response return valid data" ) - + self.assertNotEqual( len(virtual_machines), 0, @@ -678,7 +678,7 @@ class TestRouterStopCreatePF(cloudstackTestCase): router = routers[0] self.debug("Stopping router ID: %s" % router.id) - + #Stop the router cmd = stopRouter.stopRouterCmd() cmd.id = router.id @@ -715,6 +715,16 @@ class TestRouterStopCreatePF(cloudstackTestCase): ) public_ip = public_ips[0] + # Open up firewall port for SSH + fw_rule = FireWallRule.create( + self.apiclient, + ipaddressid=public_ip.id, + protocol=self.services["natrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["natrule"]["publicport"], + endport=self.services["natrule"]["publicport"] + ) + self.debug("Creating NAT rule for VM ID: %s" % self.vm_1.id) #Create NAT rule nat_rule = NATRule.create( @@ -766,7 +776,7 @@ class TestRouterStopCreatePF(cloudstackTestCase): try: self.debug("SSH into VM with ID: %s" % nat_rule.ipaddress) - + self.vm_1.ssh_port = nat_rule.publicport self.vm_1.get_ssh_client(nat_rule.ipaddress) except Exception as e: @@ -864,7 +874,7 @@ class TestRouterStopCreateLB(cloudstackTestCase): True, "Check for list routers response return valid data" ) - + self.assertNotEqual( len(routers), 0, @@ -908,6 +918,16 @@ class TestRouterStopCreateLB(cloudstackTestCase): "Check for list public IPs response return valid data" ) public_ip = public_ips[0] + + # Open up firewall port for SSH + fw_rule = FireWallRule.create( + self.apiclient, + ipaddressid=public_ip.id, + protocol=self.services["lbrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["lbrule"]["publicport"], + endport=self.services["lbrule"]["publicport"] + ) self.debug("Creating LB rule for public IP: %s" % public_ip.id) #Create Load Balancer rule and assign VMs to rule lb_rule = LoadBalancerRule.create( @@ -1071,7 +1091,7 @@ class TestRouterStopCreateFW(cloudstackTestCase): ) router = routers[0] - + self.debug("Stopping the router: %s" % router.id) #Stop the router cmd = stopRouter.stopRouterCmd() @@ -1135,7 +1155,7 @@ class TestRouterStopCreateFW(cloudstackTestCase): True, "Check for list routers response return valid data" ) - + router = routers[0] self.assertEqual( @@ -1153,7 +1173,7 @@ class TestRouterStopCreateFW(cloudstackTestCase): True, "Check for list FW rules response return valid data" ) - + self.assertEqual( fw_rules[0].state, 'Active', diff --git a/test/integration/component/test_security_groups.py b/test/integration/component/test_security_groups.py index 60cab0ee947..68074536f9e 100644 --- a/test/integration/component/test_security_groups.py +++ b/test/integration/component/test_security_groups.py @@ -18,10 +18,10 @@ import marvin from marvin.cloudstackTestCase import * from marvin.cloudstackAPI import * -from marvin.remoteSSHClient import remoteSSHClient from integration.lib.utils import * from integration.lib.base import * from integration.lib.common import * +from marvin.remoteSSHClient import remoteSSHClient #Import System modules import time @@ -1625,4 +1625,4 @@ class TestIngressRule(cloudstackTestCase): self.fail("SSH access failed for ingress rule ID: %s" \ % ingress_rule["id"] ) - return \ No newline at end of file + return diff --git a/test/integration/component/test_snapshots.py b/test/integration/component/test_snapshots.py index 2dd6d90b684..05585b5ae68 100644 --- a/test/integration/component/test_snapshots.py +++ b/test/integration/component/test_snapshots.py @@ -860,7 +860,7 @@ class TestSnapshotDetachedDisk(cloudstackTestCase): self.services["sub_lvl_dir2"], self.services["random_data"] ), - "sync", + "sync", ] for c in cmds: self.debug(ssh_client.execute(c)) diff --git a/test/integration/component/test_templates.py b/test/integration/component/test_templates.py index 82d85441855..afad16c04b7 100644 --- a/test/integration/component/test_templates.py +++ b/test/integration/component/test_templates.py @@ -20,6 +20,7 @@ from marvin.cloudstackAPI import * from integration.lib.utils import * from integration.lib.base import * from integration.lib.common import * +from marvin.remoteSSHClient import remoteSSHClient import urllib from random import random #Import System modules diff --git a/test/integration/component/test_volumes.py b/test/integration/component/test_volumes.py index 33c6b1ce4c0..4caee7d170f 100644 --- a/test/integration/component/test_volumes.py +++ b/test/integration/component/test_volumes.py @@ -743,7 +743,7 @@ class TestAttachVolumeISO(cloudstackTestCase): iso.id, self.account.account.name )) - self.cleanup.append(iso) + try: self.debug("Downloading ISO with ID: %s" % iso.id) iso.download(self.apiclient) diff --git a/test/integration/lib/base.py b/test/integration/lib/base.py index 11f01d2a607..1f8a2f851c0 100644 --- a/test/integration/lib/base.py +++ b/test/integration/lib/base.py @@ -9,7 +9,7 @@ # 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. -# +# # Automatically generated by addcopyright.py at 04/03/2012 """ Base class for all Cloudstack resources @@ -23,7 +23,6 @@ from marvin.cloudstackAPI import * import time import hashlib import base64 -import types class Domain: @@ -174,9 +173,9 @@ class VirtualMachine: domainid=None, networkids=None, serviceofferingid=None, securitygroupids=None, projectid=None, mode='basic'): """Create the instance""" - + cmd = deployVirtualMachine.deployVirtualMachineCmd() - + if serviceofferingid: cmd.serviceofferingid = serviceofferingid elif "serviceoffering" in services: @@ -207,21 +206,21 @@ class VirtualMachine: if "diskoffering" in services: cmd.diskofferingid = services["diskoffering"] - + if securitygroupids: cmd.securitygroupids = [str(sg_id) for sg_id in securitygroupids] - + if "userdata" in services: cmd.userdata = base64.b64encode(services["userdata"]) - + if projectid: cmd.projectid = projectid virtual_machine = apiclient.deployVirtualMachine(cmd) - + # VM should be in Running state after deploy timeout = 10 - while True: + while True: vm_status = VirtualMachine.list( apiclient, id=virtual_machine.id @@ -231,12 +230,12 @@ class VirtualMachine: break elif timeout == 0: raise Exception( - "TimeOutException: Failed to start VM (ID: %s)" % + "TimeOutException: Failed to start VM (ID: %s)" % virtual_machine.id) - + time.sleep(10) - timeout = timeout -1 - + timeout = timeout - 1 + if mode.lower() == 'advanced': public_ip = PublicIPAddress.create( apiclient, @@ -245,6 +244,14 @@ class VirtualMachine: virtual_machine.domainid, services ) + fw_rule = FireWallRule.create( + apiclient, + ipaddressid=public_ip.ipaddress.id, + protocol='TCP', + cidrlist=['0.0.0.0/0'], + startport=22, + endport=22 + ) nat_rule = NATRule.create( apiclient, virtual_machine, @@ -329,6 +336,20 @@ class VirtualMachine: [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listVirtualMachines(cmd)) + def resetPassword(self, apiclient): + """Resets VM password if VM created using password enabled template""" + + cmd = resetPasswordForVirtualMachine.resetPasswordForVirtualMachineCmd() + cmd.id = self.id + try: + response = apiclient.resetPasswordForVirtualMachine(cmd) + print response + except Exception as e: + raise Exception("Reset Password failed! - %s" % e) + print type(response) + if isinstance(response, list): + return response[0].password + class Volume: """Manage Volume Lifecycle @@ -337,8 +358,8 @@ class Volume: self.__dict__.update(items) @classmethod - def create(cls, apiclient, services, zoneid=None, account=None, domainid=None, - diskofferingid=None, projectid=None): + def create(cls, apiclient, services, zoneid=None, account=None, + domainid=None, diskofferingid=None, projectid=None): """Create Volume""" cmd = createVolume.createVolumeCmd() cmd.name = services["diskname"] @@ -368,24 +389,25 @@ class Volume: return Volume(apiclient.createVolume(cmd).__dict__) @classmethod - def create_custom_disk(cls, apiclient, services, account=None, domainid=None): + def create_custom_disk(cls, apiclient, services, + account=None, domainid=None): """Create Volume from Custom disk offering""" cmd = createVolume.createVolumeCmd() cmd.name = services["diskname"] cmd.diskofferingid = services["customdiskofferingid"] cmd.size = services["customdisksize"] cmd.zoneid = services["zoneid"] - + if account: cmd.account = account else: cmd.account = services["account"] - + if domainid: cmd.domainid = domainid else: cmd.domainid = services["domainid"] - + return Volume(apiclient.createVolume(cmd).__dict__) @classmethod @@ -429,7 +451,8 @@ class Snapshot: self.__dict__.update(items) @classmethod - def create(cls, apiclient, volume_id, account=None, domainid=None, projectid=None): + def create(cls, apiclient, volume_id, account=None, + domainid=None, projectid=None): """Create Snapshot""" cmd = createSnapshot.createSnapshotCmd() cmd.volumeid = volume_id @@ -476,7 +499,8 @@ class Template: cmd.ispublic = services["ispublic"] if "ispublic" in services else False cmd.isextractable = services["isextractable"] if "isextractable" in services else False cmd.passwordenabled = services["passwordenabled"] if "passwordenabled" in services else False - + cmd.passwordenabled = services["passwordenabled"] if "passwordenabled" in services else False + if volumeid: cmd.volumeid = volumeid @@ -491,9 +515,10 @@ class Template: return Template(apiclient.createTemplate(cmd).__dict__) @classmethod - def register(cls, apiclient, services, zoneid=None, account=None, domainid=None): + def register(cls, apiclient, services, zoneid=None, + account=None, domainid=None): """Create template from URL""" - + #Create template from Virtual machine and Volume ID cmd = registerTemplate.registerTemplateCmd() cmd.displaytext = services["displaytext"] @@ -502,7 +527,7 @@ class Template: cmd.hypervisor = services["hypervisor"] cmd.ostypeid = services["ostypeid"] cmd.url = services["url"] - + if zoneid: cmd.zoneid = zoneid else: @@ -511,27 +536,29 @@ class Template: cmd.isfeatured = services["isfeatured"] if "isfeatured" in services else False cmd.ispublic = services["ispublic"] if "ispublic" in services else False cmd.isextractable = services["isextractable"] if "isextractable" in services else False + cmd.passwordenabled = services["passwordenabled"] if "passwordenabled" in services else False if account: cmd.account = account if domainid: cmd.domainid = domainid - + # Register Template template = apiclient.registerTemplate(cmd) - + if isinstance(template, list): return Template(template[0].__dict__) @classmethod - def create_from_snapshot(cls, apiclient, snapshot, services, random_name=True): + def create_from_snapshot(cls, apiclient, snapshot, services, + random_name=True): """Create Template from snapshot""" #Create template from Virtual machine and Snapshot ID cmd = createTemplate.createTemplateCmd() cmd.displaytext = services["displaytext"] cmd.name = "-".join([ - services["name"], + services["name"], random_gen() ]) if random_name else services["name"] cmd.ostypeid = services["ostypeid"] @@ -540,7 +567,7 @@ class Template: def delete(self, apiclient): """Delete Template""" - + cmd = deleteTemplate.deleteTemplateCmd() cmd.id = self.id apiclient.deleteTemplate(cmd) @@ -549,7 +576,7 @@ class Template: """Download Template""" #Sleep to ensure template is in proper state before download time.sleep(interval) - + while True: template_response = Template.list( apiclient, @@ -558,15 +585,15 @@ class Template: templatefilter='self' ) if isinstance(template_response, list): - + template = template_response[0] # If template is ready, # template.status = Download Complete # Downloading - x% Downloaded - # Error - Any other string + # Error - Any other string if template.status == 'Download Complete': break - + elif 'Downloaded' in template.status: time.sleep(interval) @@ -575,7 +602,7 @@ class Template: elif timeout == 0: break - + else: time.sleep(interval) timeout = timeout - 1 @@ -583,7 +610,7 @@ class Template: def updatePermissions(self, apiclient, **kwargs): """Updates the template permissions""" - + cmd = updateTemplatePermissions.updateTemplatePermissionsCmd() cmd.id = self.id [setattr(cmd, k, v) for k, v in kwargs.items()] @@ -605,7 +632,7 @@ class Iso: self.__dict__.update(items) @classmethod - def create(cls, apiclient, services, account=None, domainid=None, + def create(cls, apiclient, services, account=None, domainid=None, projectid=None): """Create an ISO""" #Create ISO from URL @@ -615,7 +642,7 @@ class Iso: cmd.ostypeid = services["ostypeid"] cmd.url = services["url"] cmd.zoneid = services["zoneid"] - + if "isextractable" in services: cmd.isextractable = services["isextractable"] if "isfeatured" in services: @@ -631,7 +658,7 @@ class Iso: cmd.projectid = projectid # Register ISO iso = apiclient.registerIso(cmd) - + if iso: return Iso(iso[0].__dict__) @@ -651,17 +678,18 @@ class Iso: cmd = listIsos.listIsosCmd() cmd.id = self.id iso_response = apiclient.listIsos(cmd) - - if isinstance(iso_response, list): + + if isinstance(iso_response, list): response = iso_response[0] # Again initialize timeout to avoid listISO failure timeout = 5 - + print response.status # Check whether download is in progress(for Ex:10% Downloaded) # or ISO is 'Successfully Installed' if response.status == 'Successfully Installed': return - elif 'Downloaded' not in response.status: + elif 'Downloaded' not in response.status and \ + 'Installing' not in response.status: raise Exception("ErrorInDownload") elif timeout == 0: @@ -690,15 +718,15 @@ class PublicIPAddress: services=None, networkid=None, projectid=None): """Associate Public IP address""" cmd = associateIpAddress.associateIpAddressCmd() - + if accountid: cmd.account = accountid - + if zoneid: cmd.zoneid = zoneid elif "zoneid" in services: services["zoneid"] - + if domainid: cmd.domainid = domainid elif "domainid" in services: @@ -706,7 +734,7 @@ class PublicIPAddress: if networkid: cmd.networkid = networkid - + if projectid: cmd.projectid = projectid return PublicIPAddress(apiclient.associateIpAddress(cmd).__dict__) @@ -726,6 +754,7 @@ class PublicIPAddress: [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listPublicIpAddresses(cmd)) + class NATRule: """Manage port forwarding rule""" @@ -747,10 +776,10 @@ class NATRule: cmd.publicport = services["publicport"] cmd.protocol = services["protocol"] cmd.virtualmachineid = virtual_machine.id - + if projectid: cmd.projectid = projectid - + return NATRule(apiclient.createPortForwardingRule(cmd).__dict__) def delete(self, apiclient): @@ -767,7 +796,7 @@ class NATRule: cmd = listPortForwardingRules.listPortForwardingRulesCmd() [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listPortForwardingRules(cmd)) - + class StaticNATRule: """Manage Static NAT rule""" @@ -778,22 +807,22 @@ class StaticNATRule: @classmethod def create(cls, apiclient, services, ipaddressid=None): """Creates static ip forwarding rule""" - + cmd = createIpForwardingRule.createIpForwardingRuleCmd() cmd.protocol = services["protocol"] cmd.startport = services["startport"] - + if "endport" in services: cmd.endport = services["endport"] - + if "cidrlist" in services: cmd.cidrlist = services["cidrlist"] - + if ipaddressid: cmd.ipaddressid = ipaddressid elif "ipaddressid" in services: cmd.ipaddressid = services["ipaddressid"] - + return StaticNATRule(apiclient.createIpForwardingRule(cmd).__dict__) def delete(self, apiclient): @@ -810,26 +839,26 @@ class StaticNATRule: cmd = listIpForwardingRules.listIpForwardingRulesCmd() [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listIpForwardingRules(cmd)) - + @classmethod def enable(cls, apiclient, ipaddressid, virtualmachineid): """Enables Static NAT rule""" - + cmd = enableStaticNat.enableStaticNatCmd() cmd.ipaddressid = ipaddressid cmd.virtualmachineid = virtualmachineid apiclient.enableStaticNat(cmd) return - + @classmethod def disable(cls, apiclient, ipaddressid, virtualmachineid): """Disables Static NAT rule""" - + cmd = disableStaticNat.disableStaticNatCmd() cmd.ipaddressid = ipaddressid apiclient.disableStaticNat(cmd) return - + class FireWallRule: """Manage Firewall rule""" @@ -910,6 +939,7 @@ class ServiceOffering: [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listServiceOfferings(cmd)) + class DiskOffering: """Manage disk offerings cycle""" @@ -957,7 +987,7 @@ class NetworkOffering: @classmethod def create(cls, apiclient, services, **kwargs): """Create network offering""" - + cmd = createNetworkOffering.createNetworkOfferingCmd() cmd.displaytext = "-".join([services["displaytext"], random_gen()]) cmd.name = "-".join([services["name"], random_gen()]) @@ -968,12 +998,25 @@ class NetworkOffering: cmd.serviceProviderList = [] for service, provider in services["serviceProviderList"].items(): cmd.serviceProviderList.append({ - 'service' : service, + 'service': service, 'provider': provider }) + if "servicecapabilitylist" in services: + cmd.servicecapabilitylist = [] + for service, capability in services["servicecapabilitylist"].items(): + for ctype, value in capability.items(): + cmd.servicecapabilitylist.append({ + 'service': service, + 'capabilitytype': ctype, + 'capabilityvalue': value + }) + if "specifyVlan" in services: + cmd.specifyVlan = services["specifyVlan"] + if "specifyIpRanges" in services: + cmd.specifyIpRanges = services["specifyIpRanges"] [setattr(cmd, k, v) for k, v in kwargs.items()] - + return NetworkOffering(apiclient.createNetworkOffering(cmd).__dict__) def delete(self, apiclient): @@ -1040,23 +1083,30 @@ class LoadBalancerRule: self.__dict__.update(items) @classmethod - def create(cls, apiclient, services, ipaddressid, accountid=None, - networkid=None, projectid=None): + def create(cls, apiclient, services, ipaddressid=None, accountid=None, + networkid=None, projectid=None, domainid=None): """Create Load balancing Rule""" cmd = createLoadBalancerRule.createLoadBalancerRuleCmd() - cmd.publicipid = ipaddressid or services["ipaddressid"] + + if ipaddressid: + cmd.publicipid = ipaddressid + elif "ipaddressid" in services: + cmd.publicipid = services["ipaddressid"] if accountid: cmd.account = accountid elif "account" in services: cmd.account = services["account"] + if domainid: + cmd.domainid = domainid + cmd.name = services["name"] cmd.algorithm = services["alg"] cmd.privateport = services["privateport"] cmd.publicport = services["publicport"] - + if "openfirewall" in services: cmd.openfirewall = services["openfirewall"] @@ -1090,6 +1140,19 @@ class LoadBalancerRule: apiclient.removeFromLoadBalancerRule(cmd) return + def update(self, apiclient, algorithm=None, description=None, name=None): + """Updates the load balancing rule""" + cmd = updateLoadBalancerRule.updateLoadBalancerRuleCmd() + cmd.id = self.id + if algorithm: + cmd.algorithm = algorithm + if description: + cmd.description = description + if name: + cmd.name = name + + return apiclient.updateLoadBalancerRule(cmd) + @classmethod def list(cls, apiclient, **kwargs): """List all Load balancing rules matching criteria""" @@ -1111,12 +1174,12 @@ class Cluster: cmd = addCluster.addClusterCmd() cmd.clustertype = services["clustertype"] cmd.hypervisor = services["hypervisor"] - + if zoneid: cmd.zoneid = zoneid else: cmd.zoneid = services["zoneid"] - + if podid: cmd.podid = podid else: @@ -1158,17 +1221,17 @@ class Host: @classmethod def create(cls, apiclient, cluster, services, zoneid=None, podid=None): """Create Host in cluster""" - + cmd = addHost.addHostCmd() cmd.hypervisor = services["hypervisor"] cmd.url = services["url"] cmd.clusterid = cluster.id - + if zoneid: cmd.zoneid = zoneid else: cmd.zoneid = services["zoneid"] - + if podid: cmd.podid = podid else: @@ -1180,10 +1243,10 @@ class Host: cmd.username = services["username"] if "password" in services: cmd.password = services["password"] - + # Add host host = apiclient.addHost(cmd) - + if isinstance(host, list): return Host(host[0].__dict__) @@ -1202,7 +1265,7 @@ class Host: def enableMaintenance(self, apiclient): """enables maintainance mode Host""" - + cmd = prepareHostForMaintenance.prepareHostForMaintenanceCmd() cmd.id = self.id return apiclient.prepareHostForMaintenance(cmd) @@ -1223,23 +1286,24 @@ class StoragePool: self.__dict__.update(items) @classmethod - def create(cls, apiclient, services, clusterid=None, zoneid=None, podid=None): + def create(cls, apiclient, services, clusterid=None, + zoneid=None, podid=None): """Create Storage pool (Primary Storage)""" cmd = createStoragePool.createStoragePoolCmd() cmd.name = services["name"] - + if podid: cmd.podid = podid else: cmd.podid = services["podid"] - + cmd.url = services["url"] if clusterid: cmd.clusterid = clusterid elif "clusterid" in services: cmd.clusterid = services["clusterid"] - + if zoneid: cmd.zoneid = zoneid else: @@ -1262,7 +1326,7 @@ class StoragePool: def enableMaintenance(self, apiclient): """enables maintainance mode Storage pool""" - + cmd = enableStorageMaintenance.enableStorageMaintenanceCmd() cmd.id = self.id return apiclient.enableStorageMaintenance(cmd) @@ -1283,23 +1347,23 @@ class Network: self.__dict__.update(items) @classmethod - def create(cls, apiclient, services, accountid=None, domainid=None, + def create(cls, apiclient, services, accountid=None, domainid=None, networkofferingid=None, projectid=None, zoneid=None): """Create Network for account""" cmd = createNetwork.createNetworkCmd() cmd.name = services["name"] cmd.displaytext = services["displaytext"] - + if networkofferingid: cmd.networkofferingid = networkofferingid elif "networkoffering" in services: cmd.networkofferingid = services["networkoffering"] - + if zoneid: cmd.zoneid = zoneid elif "zoneid" in services: cmd.zoneid = services["zoneid"] - + if "gateway" in services: cmd.gateway = services["gateway"] if "netmask" in services: @@ -1312,7 +1376,7 @@ class Network: cmd.vlan = services["vlan"] if "acltype" in services: cmd.acltype = services["acltype"] - + if accountid: cmd.account = accountid if domainid: @@ -1353,7 +1417,7 @@ class Vpn: self.__dict__.update(items) @classmethod - def create(cls, apiclient, publicipid, account=None, domainid=None, + def create(cls, apiclient, publicipid, account=None, domainid=None, projectid=None): """Create VPN for Public IP address""" cmd = createRemoteAccessVpn.createRemoteAccessVpnCmd() @@ -1455,13 +1519,12 @@ class Zone: def update(self, apiclient, **kwargs): """Update the zone""" - + cmd = updateZone.updateZoneCmd() cmd.id = self.id [setattr(cmd, k, v) for k, v in kwargs.items()] return apiclient.updateZone(cmd) - - + @classmethod def list(cls, apiclient, **kwargs): """List all Zones matching criteria""" @@ -1515,7 +1578,7 @@ class PublicIpRange: @classmethod def create(cls, apiclient, services): """Create VlanIpRange""" - + cmd = createVlanIpRange.createVlanIpRangeCmd() cmd.gateway = services["gateway"] cmd.netmask = services["netmask"] @@ -1566,7 +1629,7 @@ class SecondaryStorage: cmd = deleteHost.deleteHostCmd() cmd.id = self.id apiclient.deleteHost(cmd) - + class PhysicalNetwork: """Manage physical network storage""" @@ -1594,20 +1657,28 @@ class PhysicalNetwork: def update(self, apiclient, **kwargs): """Update Physical network state""" - + cmd = updatePhysicalNetwork.updatePhysicalNetworkCmd() cmd.id = self.id [setattr(cmd, k, v) for k, v in kwargs.items()] return apiclient.updatePhysicalNetwork(cmd) - + def addTrafficType(self, apiclient, type): """Add Traffic type to Physical network""" - + cmd = addTrafficType.addTrafficTypeCmd() cmd.physicalnetworkid = self.id cmd.traffictype = type return apiclient.addTrafficType(cmd) + @classmethod + def list(cls, apiclient, **kwargs): + """Lists all physical networks""" + + cmd = listPhysicalNetworks.listPhysicalNetworksCmd() + [setattr(cmd, k, v) for k, v in kwargs.items()] + return(apiclient.listPhysicalNetworks(cmd)) + class SecurityGroup: """Manage Security Groups""" @@ -1624,9 +1695,9 @@ class SecurityGroup: if account: cmd.account = account if domainid: - cmd.domainid=domainid + cmd.domainid = domainid if description: - cmd.description=description + cmd.description = description if projectid: cmd.projectid = projectid @@ -1638,79 +1709,79 @@ class SecurityGroup: cmd = deleteSecurityGroup.deleteSecurityGroupCmd() cmd.id = self.id apiclient.deleteSecurityGroup(cmd) - + def authorize(self, apiclient, services, account=None, domainid=None, projectid=None): """Authorize Ingress Rule""" - - cmd=authorizeSecurityGroupIngress.authorizeSecurityGroupIngressCmd() - + + cmd = authorizeSecurityGroupIngress.authorizeSecurityGroupIngressCmd() + if domainid: cmd.domainid = domainid if account: cmd.account = account if projectid: - cmd.projectid = projectid - cmd.securitygroupid=self.id - cmd.protocol=services["protocol"] - + cmd.projectid = projectid + cmd.securitygroupid = self.id + cmd.protocol = services["protocol"] + if services["protocol"] == 'ICMP': cmd.icmptype = -1 cmd.icmpcode = -1 else: cmd.startport = services["startport"] cmd.endport = services["endport"] - + cmd.cidrlist = services["cidrlist"] return (apiclient.authorizeSecurityGroupIngress(cmd).__dict__) - + def revoke(self, apiclient, id): """Revoke ingress rule""" - - cmd=revokeSecurityGroupIngress.revokeSecurityGroupIngressCmd() - cmd.id=id + + cmd = revokeSecurityGroupIngress.revokeSecurityGroupIngressCmd() + cmd.id = id return apiclient.revokeSecurityGroupIngress(cmd) def authorizeEgress(self, apiclient, services, account=None, domainid=None, - projectid=None, user_secgrp_list = {}): + projectid=None, user_secgrp_list={}): """Authorize Egress Rule""" - - cmd=authorizeSecurityGroupEgress.authorizeSecurityGroupEgressCmd() - + + cmd = authorizeSecurityGroupEgress.authorizeSecurityGroupEgressCmd() + if domainid: cmd.domainid = domainid if account: cmd.account = account if projectid: - cmd.projectid = projectid - cmd.securitygroupid=self.id - cmd.protocol=services["protocol"] - + cmd.projectid = projectid + cmd.securitygroupid = self.id + cmd.protocol = services["protocol"] + if services["protocol"] == 'ICMP': cmd.icmptype = -1 cmd.icmpcode = -1 else: cmd.startport = services["startport"] cmd.endport = services["endport"] - + cmd.cidrlist = services["cidrlist"] - + cmd.usersecuritygrouplist = [] for account, group in user_secgrp_list.items(): cmd.usersecuritygrouplist.append({ - 'account' : account, + 'account': account, 'group': group }) return (apiclient.authorizeSecurityGroupEgress(cmd).__dict__) - + def revokeEgress(self, apiclient, id): """Revoke Egress rule""" - - cmd=revokeSecurityGroupEgress.revokeSecurityGroupEgressCmd() - cmd.id=id + + cmd = revokeSecurityGroupEgress.revokeSecurityGroupEgressCmd() + cmd.id = id return apiclient.revokeSecurityGroupEgress(cmd) @classmethod @@ -1720,7 +1791,8 @@ class SecurityGroup: cmd = listSecurityGroups.listSecurityGroupsCmd() [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listSecurityGroups(cmd)) - + + class Project: """Manage Project life cycle""" @@ -1730,7 +1802,7 @@ class Project: @classmethod def create(cls, apiclient, services, account=None, domainid=None): """Create project""" - + cmd = createProject.createProjectCmd() cmd.displaytext = services["displaytext"] cmd.name = "-".join([services["name"], random_gen()]) @@ -1740,17 +1812,17 @@ class Project: cmd.domainid = domainid return Project(apiclient.createProject(cmd).__dict__) - + def delete(self, apiclient): """Delete Project""" cmd = deleteProject.deleteProjectCmd() cmd.id = self.id apiclient.deleteProject(cmd) - + def update(self, apiclient, **kwargs): """Updates the project""" - + cmd = updateProject.updateProjectCmd() cmd.id = self.id [setattr(cmd, k, v) for k, v in kwargs.items()] @@ -1758,21 +1830,21 @@ class Project: def activate(self, apiclient): """Activates the suspended project""" - + cmd = activateProject.activateProjectCmd() cmd.id = self.id return apiclient.activateProject(cmd) - + def suspend(self, apiclient): """Suspend the active project""" - + cmd = suspendProject.suspendProjectCmd() cmd.id = self.id return apiclient.suspendProject(cmd) def addAccount(self, apiclient, account=None, email=None): """Add account to project""" - + cmd = addAccountToProject.addAccountToProjectCmd() cmd.projectid = self.id if account: @@ -1783,7 +1855,7 @@ class Project: def deleteAccount(self, apiclient, account): """Delete account from project""" - + cmd = deleteAccountFromProject.deleteAccountFromProjectCmd() cmd.projectid = self.id cmd.account = account @@ -1805,16 +1877,17 @@ class Project: [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listProjects(cmd)) + class ProjectInvitation: """Manage project invitations""" def __init__(self, items): self.__dict__.update(items) - + @classmethod def update(cls, apiclient, projectid, accept, account=None, token=None): """Updates the project invitation for that account""" - + cmd = updateProjectInvitation.updateProjectInvitationCmd() cmd.projectid = projectid cmd.accept = accept @@ -1822,20 +1895,20 @@ class ProjectInvitation: cmd.account = account if token: cmd.token = token - + return (apiclient.updateProjectInvitation(cmd).__dict__) - + def delete(self, apiclient, id): """Deletes the project invitation""" - + cmd = deleteProjectInvitation.deleteProjectInvitationCmd() cmd.id = id return apiclient.deleteProjectInvitation(cmd) - + @classmethod def list(cls, apiclient, **kwargs): """Lists project invitations""" - + cmd = listProjectInvitations.listProjectInvitationsCmd() [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listProjectInvitations(cmd)) @@ -1843,20 +1916,132 @@ class ProjectInvitation: class Configurations: """Manage Configuration""" - + @classmethod def update(cls, apiclient, name, value=None): """Updates the specified configuration""" - + cmd = updateConfiguration.updateConfigurationCmd() cmd.name = name cmd.value = value apiclient.updateConfiguration(cmd) - + @classmethod def list(cls, apiclient, **kwargs): """Lists configurations""" - + cmd = listConfigurations.listConfigurationsCmd() [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listConfigurations(cmd)) + + +class NetScaler: + """Manage external netscaler device""" + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def add(cls, apiclient, services, physicalnetworkid, username=None, password=None): + """Add external netscaler device to cloudstack""" + + cmd = addNetscalerLoadBalancer.addNetscalerLoadBalancerCmd() + cmd.physicalnetworkid = physicalnetworkid + if username: + cmd.username = username + else: + cmd.username = services["username"] + + if password: + cmd.password = password + else: + cmd.password = services["password"] + + cmd.networkdevicetype = services["networkdevicetype"] + + # Generate the URL + url = 'https://' + str(services["ipaddress"]) + '?' + url = url + 'publicinterface=' + str(services["publicinterface"]) + '&' + url = url + 'privateinterface=' + str(services["privateinterface"]) + '&' + url = url + 'numretries=' + str(services["numretries"]) + '&' + + if not services["lbdevicededicated"] and "lbdevicecapacity" in services: + url = url + 'lbdevicecapacity=' + str(services["lbdevicecapacity"]) + '&' + + url = url + 'lbdevicededicated=' + str(services["lbdevicededicated"]) + + cmd.url = url + return NetScaler(apiclient.addNetscalerLoadBalancer(cmd).__dict__) + + def delete(self, apiclient): + """Deletes a netscaler device from CloudStack""" + + cmd = deleteNetscalerLoadBalancer.deleteNetscalerLoadBalancerCmd() + cmd.lbdeviceid = self.lbdeviceid + apiclient.deleteNetscalerLoadBalancer(cmd) + return + + def configure(self, apiclient, **kwargs): + """List already registered netscaler devices""" + + cmd = configureNetscalerLoadBalancer.configureNetscalerLoadBalancerCmd() + cmd.lbdeviceid = self.lbdeviceid + [setattr(cmd, k, v) for k, v in kwargs.items()] + return(apiclient.configureNetscalerLoadBalancer(cmd)) + + @classmethod + def list(cls, apiclient, **kwargs): + """List already registered netscaler devices""" + + cmd = listNetscalerLoadBalancers.listNetscalerLoadBalancersCmd() + [setattr(cmd, k, v) for k, v in kwargs.items()] + return(apiclient.listNetscalerLoadBalancers(cmd)) + + +class NetworkServiceProvider: + """Manage network serivce providers for CloudStack""" + + def __init__(self, items): + self.__dict__.update(items) + + @classmethod + def add(cls, apiclient, name, physicalnetworkid, servicelist): + """Adds network service provider""" + + cmd = addNetworkServiceProvider.addNetworkServiceProviderCmd() + cmd.name = name + cmd.physicalnetworkid = physicalnetworkid + cmd.servicelist = servicelist + return NetworkServiceProvider(apiclient.addNetworkServiceProvider(cmd).__dict__) + + def delete(self, apiclient): + """Deletes network service provider""" + + cmd = deleteNetworkServiceProvider.deleteNetworkServiceProviderCmd() + cmd.id = self.id + return apiclient.deleteNetworkServiceProvider(cmd) + + def update(self, apiclient, **kwargs): + """Updates network service provider""" + + cmd = updateNetworkServiceProvider.updateNetworkServiceProviderCmd() + cmd.id = self.id + [setattr(cmd, k, v) for k, v in kwargs.items()] + return apiclient.updateNetworkServiceProvider(cmd) + + @classmethod + def update(cls, apiclient, id, **kwargs): + """Updates network service provider""" + + cmd = updateNetworkServiceProvider.updateNetworkServiceProviderCmd() + cmd.id = id + [setattr(cmd, k, v) for k, v in kwargs.items()] + return apiclient.updateNetworkServiceProvider(cmd) + + @classmethod + def list(cls, apiclient, **kwargs): + """List network service providers""" + + cmd = listNetworkServiceProviders.listNetworkServiceProvidersCmd() + [setattr(cmd, k, v) for k, v in kwargs.items()] + return(apiclient.listNetworkServiceProviders(cmd)) diff --git a/test/integration/lib/common.py b/test/integration/lib/common.py index 0c84c755b6f..17f24d74770 100644 --- a/test/integration/lib/common.py +++ b/test/integration/lib/common.py @@ -9,7 +9,7 @@ # 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. -# +# # Automatically generated by addcopyright.py at 04/03/2012 """Common functions """ @@ -25,6 +25,7 @@ from base import * #Import System modules import time + def get_domain(apiclient, services=None): "Returns a default domain" @@ -32,13 +33,14 @@ def get_domain(apiclient, services=None): if services: if "domainid" in services: cmd.id = services["domainid"] - + domains = apiclient.listDomains(cmd) - + if isinstance(domains, list): return domains[0] else: - raise Exception("Failed to find specified domain.") + raise Exception("Failed to find specified domain.") + def get_zone(apiclient, services=None): "Returns a default zone" @@ -47,13 +49,14 @@ def get_zone(apiclient, services=None): if services: if "zoneid" in services: cmd.id = services["zoneid"] - + zones = apiclient.listZones(cmd) - + if isinstance(zones, list): return zones[0] else: - raise Exception("Failed to find specified zone.") + raise Exception("Failed to find specified zone.") + def get_pod(apiclient, zoneid, services=None): "Returns a default pod for specified zone" @@ -64,13 +67,14 @@ def get_pod(apiclient, zoneid, services=None): if services: if "podid" in services: cmd.id = services["podid"] - + pods = apiclient.listPods(cmd) - + if isinstance(pods, list): return pods[0] else: - raise Exception("Exception: Failed to find specified pod.") + raise Exception("Exception: Failed to find specified pod.") + def get_template(apiclient, zoneid, ostypeid=12, services=None): "Returns a template" @@ -88,11 +92,12 @@ def get_template(apiclient, zoneid, ostypeid=12, services=None): for template in list_templates: if template.ostypeid == ostypeid: return template - + raise Exception("Exception: Failed to find template with OSTypeID: %s" % - ostypeid) + ostypeid) return + def download_systemplates_sec_storage(server, services): """Download System templates on sec storage""" @@ -104,7 +109,7 @@ def download_systemplates_sec_storage(server, services): server["username"], server["password"] ) - except Exception as e: + except Exception: raise Exception("SSH access failted for server with IP address: %s" % server["ipaddess"]) # Mount Secondary Storage on Management Server @@ -136,6 +141,7 @@ def download_systemplates_sec_storage(server, services): raise Exception("Failed to download System Templates on Sec Storage") return + def wait_for_ssvms(apiclient, zoneid, podid, interval=60): """After setup wait for SSVMs to come Up""" @@ -179,18 +185,20 @@ def wait_for_ssvms(apiclient, zoneid, podid, interval=60): break return -def download_builtin_templates(apiclient, zoneid, hypervisor, host, linklocalip, interval=60): + +def download_builtin_templates(apiclient, zoneid, hypervisor, host, + linklocalip, interval=60): """After setup wait till builtin templates are downloaded""" - + # Change IPTABLES Rules - result = get_process_status( - host["ipaddress"], - host["port"], - host["username"], - host["password"], - linklocalip, - "iptables -P INPUT ACCEPT" - ) + get_process_status( + host["ipaddress"], + host["port"], + host["username"], + host["password"], + linklocalip, + "iptables -P INPUT ACCEPT" + ) time.sleep(interval) # Find the BUILTIN Templates for given Zone, Hypervisor list_template_response = list_templates( @@ -199,10 +207,10 @@ def download_builtin_templates(apiclient, zoneid, hypervisor, host, linklocalip, zoneid=zoneid, templatefilter='self' ) - + if not isinstance(list_template_response, list): raise Exception("Failed to download BUILTIN templates") - + # Ensure all BUILTIN templates are downloaded templateid = None for template in list_template_response: @@ -223,20 +231,21 @@ def download_builtin_templates(apiclient, zoneid, hypervisor, host, linklocalip, # If template is ready, # template.status = Download Complete # Downloading - x% Downloaded - # Error - Any other string + # Error - Any other string if template.status == 'Download Complete': break - + elif 'Downloaded' in template.status: time.sleep(interval) elif 'Installing' not in template.status: raise Exception("ErrorInDownload") - + return -def update_resource_limit(apiclient, resourcetype, account=None, domainid=None, - max=None, projectid=None): + +def update_resource_limit(apiclient, resourcetype, account=None, + domainid=None, max=None, projectid=None): """Updates the resource limit to 'max' for given account""" cmd = updateResourceLimit.updateResourceLimitCmd() @@ -252,6 +261,7 @@ def update_resource_limit(apiclient, resourcetype, account=None, domainid=None, apiclient.updateResourceLimit(cmd) return + def list_routers(apiclient, **kwargs): """List all Routers matching criteria""" @@ -259,6 +269,7 @@ def list_routers(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listRouters(cmd)) + def list_zones(apiclient, **kwargs): """List all Zones matching criteria""" @@ -266,6 +277,7 @@ def list_zones(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listZones(cmd)) + def list_networks(apiclient, **kwargs): """List all Networks matching criteria""" @@ -273,6 +285,7 @@ def list_networks(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listNetworks(cmd)) + def list_clusters(apiclient, **kwargs): """List all Clusters matching criteria""" @@ -280,6 +293,7 @@ def list_clusters(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listClusters(cmd)) + def list_ssvms(apiclient, **kwargs): """List all SSVMs matching criteria""" @@ -287,6 +301,7 @@ def list_ssvms(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listSystemVms(cmd)) + def list_storage_pools(apiclient, **kwargs): """List all storage pools matching criteria""" @@ -294,6 +309,7 @@ def list_storage_pools(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listStoragePools(cmd)) + def list_virtual_machines(apiclient, **kwargs): """List all VMs matching criteria""" @@ -301,6 +317,7 @@ def list_virtual_machines(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listVirtualMachines(cmd)) + def list_hosts(apiclient, **kwargs): """List all Hosts matching criteria""" @@ -308,6 +325,7 @@ def list_hosts(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listHosts(cmd)) + def list_configurations(apiclient, **kwargs): """List configuration with specified name""" @@ -315,6 +333,7 @@ def list_configurations(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listConfigurations(cmd)) + def list_publicIP(apiclient, **kwargs): """List all Public IPs matching criteria""" @@ -322,6 +341,7 @@ def list_publicIP(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listPublicIpAddresses(cmd)) + def list_nat_rules(apiclient, **kwargs): """List all NAT rules matching criteria""" @@ -329,6 +349,7 @@ def list_nat_rules(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listPortForwardingRules(cmd)) + def list_lb_rules(apiclient, **kwargs): """List all Load balancing rules matching criteria""" @@ -336,6 +357,7 @@ def list_lb_rules(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listLoadBalancerRules(cmd)) + def list_lb_instances(apiclient, **kwargs): """List all Load balancing instances matching criteria""" @@ -343,6 +365,7 @@ def list_lb_instances(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listLoadBalancerRuleInstances(cmd)) + def list_firewall_rules(apiclient, **kwargs): """List all Firewall Rules matching criteria""" @@ -350,6 +373,7 @@ def list_firewall_rules(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listFirewallRules(cmd)) + def list_volumes(apiclient, **kwargs): """List all volumes matching criteria""" @@ -357,6 +381,7 @@ def list_volumes(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listVolumes(cmd)) + def list_isos(apiclient, **kwargs): """Lists all available ISO files.""" @@ -364,6 +389,7 @@ def list_isos(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listIsos(cmd)) + def list_snapshots(apiclient, **kwargs): """List all snapshots matching criteria""" @@ -371,6 +397,7 @@ def list_snapshots(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listSnapshots(cmd)) + def list_templates(apiclient, **kwargs): """List all templates matching criteria""" @@ -378,6 +405,7 @@ def list_templates(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listTemplates(cmd)) + def list_domains(apiclient, **kwargs): """Lists domains""" @@ -385,6 +413,7 @@ def list_domains(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listDomains(cmd)) + def list_accounts(apiclient, **kwargs): """Lists accounts and provides detailed account information for listed accounts""" @@ -393,6 +422,7 @@ def list_accounts(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listAccounts(cmd)) + def list_users(apiclient, **kwargs): """Lists users and provides detailed account information for listed users""" @@ -401,6 +431,7 @@ def list_users(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listUsers(cmd)) + def list_snapshot_policy(apiclient, **kwargs): """Lists snapshot policies.""" @@ -408,6 +439,7 @@ def list_snapshot_policy(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listSnapshotPolicies(cmd)) + def list_events(apiclient, **kwargs): """Lists events""" @@ -415,6 +447,7 @@ def list_events(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listEvents(cmd)) + def list_disk_offering(apiclient, **kwargs): """Lists all available disk offerings.""" @@ -422,6 +455,7 @@ def list_disk_offering(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listDiskOfferings(cmd)) + def list_service_offering(apiclient, **kwargs): """Lists all available service offerings.""" @@ -429,6 +463,7 @@ def list_service_offering(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listServiceOfferings(cmd)) + def list_vlan_ipranges(apiclient, **kwargs): """Lists all VLAN IP ranges.""" @@ -436,6 +471,7 @@ def list_vlan_ipranges(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listVlanIpRanges(cmd)) + def list_usage_records(apiclient, **kwargs): """Lists usage records for accounts""" @@ -443,6 +479,7 @@ def list_usage_records(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listUsageRecords(cmd)) + def list_nw_service_prividers(apiclient, **kwargs): """Lists Network service providers""" @@ -450,6 +487,7 @@ def list_nw_service_prividers(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listNetworkServiceProviders(cmd)) + def list_virtual_router_elements(apiclient, **kwargs): """Lists Virtual Router elements""" @@ -457,6 +495,7 @@ def list_virtual_router_elements(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listVirtualRouterElements(cmd)) + def list_network_offerings(apiclient, **kwargs): """Lists network offerings""" @@ -464,9 +503,10 @@ def list_network_offerings(apiclient, **kwargs): [setattr(cmd, k, v) for k, v in kwargs.items()] return(apiclient.listNetworkOfferings(cmd)) + def list_resource_limits(apiclient, **kwargs): """Lists resource limits""" cmd = listResourceLimits.listResourceLimitsCmd() [setattr(cmd, k, v) for k, v in kwargs.items()] - return(apiclient.listResourceLimits(cmd)) \ No newline at end of file + return(apiclient.listResourceLimits(cmd)) diff --git a/test/integration/lib/utils.py b/test/integration/lib/utils.py index 3caee9b8ae4..e4a28620c7c 100644 --- a/test/integration/lib/utils.py +++ b/test/integration/lib/utils.py @@ -9,7 +9,7 @@ # 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. -# +# # Automatically generated by addcopyright.py at 04/03/2012 """Utilities functions """ @@ -28,11 +28,12 @@ import imaplib import email import datetime + def restart_mgmt_server(server): """Restarts the management server""" try: - # Get the SSH client + # Get the SSH client ssh = is_server_ssh_ready( server["ipaddress"], server["port"], @@ -49,9 +50,10 @@ def restart_mgmt_server(server): raise e return + def fetch_latest_mail(services, from_mail): """Fetch mail""" - + # Login to mail server to verify email mail = imaplib.IMAP4_SSL(services["server"]) mail.login( @@ -66,21 +68,22 @@ def fetch_latest_mail(services, from_mail): 'search', None, '(SENTSINCE {date} HEADER FROM "{mail}")'.format( - date=date, + date=date, mail=from_mail ) ) # Return False if email is not present if data == []: return False - + latest_email_uid = data[0].split()[-1] result, data = mail.uid('fetch', latest_email_uid, '(RFC822)') raw_email = data[0][1] email_message = email.message_from_string(raw_email) result = get_first_text_block(email_message) return result - + + def get_first_text_block(email_message_instance): """fetches first text block from the mail""" maintype = email_message_instance.get_content_maintype() @@ -91,15 +94,18 @@ def get_first_text_block(email_message_instance): elif maintype == 'text': return email_message_instance.get_payload() + def random_gen(size=6, chars=string.ascii_uppercase + string.digits): """Generate Random Strings of variable length""" return ''.join(random.choice(chars) for x in range(size)) + def cleanup_resources(api_client, resources): """Delete resources""" for obj in resources: obj.delete(api_client) + def is_server_ssh_ready(ipaddress, port, username, password, retries=50): """Return ssh handle else wait till sshd is running""" loop_cnt = retries @@ -129,6 +135,7 @@ def format_volume_to_ext3(ssh_client, device="/dev/sda"): for c in cmds: ssh_client.execute(c) + def fetch_api_client(config_file='datacenterCfg'): """Fetch the Cloudstack API Client""" config = configGenerator.get_setup_config(config_file) @@ -146,6 +153,7 @@ def fetch_api_client(config_file='datacenterCfg'): ) ) + def get_process_status(hostip, port, username, password, linklocalip, process): """Double hop and returns a process status""" @@ -157,20 +165,22 @@ def get_process_status(hostip, port, username, password, linklocalip, process): password ) ssh_command = "ssh -i ~/.ssh/id_rsa.cloud -ostricthostkeychecking=no " - ssh_command = ssh_command + "-oUserKnownHostsFile=/dev/null -p 3922 %s %s" \ - % (linklocalip, process) + ssh_command = ssh_command + \ + "-oUserKnownHostsFile=/dev/null -p 3922 %s %s" % ( + linklocalip, + process) # Double hop into router timeout = 5 # Ensure the SSH login is successful while True: res = ssh.execute(ssh_command) - + if res[0] != "Host key verification failed.": break elif timeout == 0: break - + time.sleep(5) timeout = timeout - 1 - return res \ No newline at end of file + return res diff --git a/test/integration/smoke/test_network.py b/test/integration/smoke/test_network.py index 7f629a6c6b0..6498b865428 100644 --- a/test/integration/smoke/test_network.py +++ b/test/integration/smoke/test_network.py @@ -97,6 +97,7 @@ class Services: # Algorithm used for load balancing "privateport": 22, "publicport": 2222, + "protocol": 'TCP' } } @@ -131,12 +132,12 @@ class TestPublicIP(cloudstackTestCase): cls.services["network"]["zoneid"] = cls.zone.id cls.network_offering = NetworkOffering.create( - cls.api_client, + cls.api_client, cls.services["network_offering"], ) # Enable Network offering cls.network_offering.update(cls.api_client, state='Enabled') - + cls.services["network"]["networkoffering"] = cls.network_offering.id cls.account_network = Network.create( cls.api_client, @@ -354,26 +355,26 @@ class TestPortForwarding(cloudstackTestCase): account=self.account.account.name, domainid=self.account.account.domainid ) - + self.assertEqual( isinstance(src_nat_ip_addrs, list), True, "Check list response returns a valid list" ) src_nat_ip_addr = src_nat_ip_addrs[0] - + # Check if VM is in Running state before creating NAT rule vm_response = VirtualMachine.list( self.apiclient, id=self.virtual_machine.id ) - + self.assertEqual( isinstance(vm_response, list), True, "Check list VM returns a valid list" ) - + self.assertNotEqual( len(vm_response), 0, @@ -384,6 +385,16 @@ class TestPortForwarding(cloudstackTestCase): 'Running', "VM state should be Running before creating a NAT rule." ) + # Open up firewall port for SSH + fw_rule = FireWallRule.create( + self.apiclient, + ipaddressid=src_nat_ip_addr.id, + protocol=self.services["natrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["natrule"]["publicport"], + endport=self.services["natrule"]["publicport"] + ) + #Create NAT rule nat_rule = NATRule.create( self.apiclient, @@ -401,7 +412,7 @@ class TestPortForwarding(cloudstackTestCase): True, "Check list response returns a valid list" ) - + self.assertNotEqual( len(list_nat_rule_response), 0, @@ -419,9 +430,9 @@ class TestPortForwarding(cloudstackTestCase): self.virtual_machine.ipaddress, src_nat_ip_addr.ipaddress )) - + self.virtual_machine.get_ssh_client(src_nat_ip_addr.ipaddress) - + except Exception as e: self.fail( "SSH Access failed for %s: %s" % \ @@ -445,7 +456,7 @@ class TestPortForwarding(cloudstackTestCase): self.debug( "SSHing into VM with IP address %s after NAT rule deletion" % self.virtual_machine.ipaddress) - + remoteSSHClient.remoteSSHClient( src_nat_ip_addr.ipaddress, self.virtual_machine.ssh_port, @@ -469,19 +480,19 @@ class TestPortForwarding(cloudstackTestCase): self.services["server"] ) self.cleanup.append(ip_address) - + # Check if VM is in Running state before creating NAT rule vm_response = VirtualMachine.list( self.apiclient, id=self.virtual_machine.id ) - + self.assertEqual( isinstance(vm_response, list), True, "Check list VM returns a valid list" ) - + self.assertNotEqual( len(vm_response), 0, @@ -492,7 +503,15 @@ class TestPortForwarding(cloudstackTestCase): 'Running', "VM state should be Running before creating a NAT rule." ) - + # Open up firewall port for SSH + fw_rule = FireWallRule.create( + self.apiclient, + ipaddressid=ip_address.ipaddress.id, + protocol=self.services["natrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["natrule"]["publicport"], + endport=self.services["natrule"]["publicport"] + ) #Create NAT rule nat_rule = NATRule.create( self.apiclient, @@ -553,7 +572,7 @@ class TestPortForwarding(cloudstackTestCase): self.debug( "SSHing into VM with IP address %s after NAT rule deletion" % self.virtual_machine.ipaddress) - + remoteSSHClient.remoteSSHClient( ip_address.ipaddress.ipaddress, self.virtual_machine.ssh_port, @@ -614,6 +633,15 @@ class TestLoadBalancingRule(cloudstackTestCase): cls.account.account.domainid, cls.services["server"] ) + # Open up firewall port for SSH + cls.fw_rule = FireWallRule.create( + cls.api_client, + ipaddressid=cls.non_src_nat_ip.ipaddress.id, + protocol=cls.services["lbrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=cls.services["lbrule"]["publicport"], + endport=cls.services["lbrule"]["publicport"] + ) cls._cleanup = [ cls.account, cls.service_offering @@ -653,20 +681,20 @@ class TestLoadBalancingRule(cloudstackTestCase): "Check list response returns a valid list" ) src_nat_ip_addr = src_nat_ip_addrs[0] - + # Check if VM is in Running state before creating LB rule vm_response = VirtualMachine.list( self.apiclient, account=self.account.account.name, domainid=self.account.account.domainid ) - + self.assertEqual( isinstance(vm_response, list), True, "Check list VM returns a valid list" ) - + self.assertNotEqual( len(vm_response), 0, @@ -678,7 +706,7 @@ class TestLoadBalancingRule(cloudstackTestCase): 'Running', "VM state should be Running before creating a NAT rule." ) - + #Create Load Balancer rule and assign VMs to rule lb_rule = LoadBalancerRule.create( self.apiclient, @@ -727,12 +755,12 @@ class TestLoadBalancingRule(cloudstackTestCase): 0, "Check Load Balancer instances Rule in its List" ) - self.debug("lb_instance_rules Ids: %s, %s" % ( + self.debug("lb_instance_rules Ids: %s, %s" % ( lb_instance_rules[0].id, lb_instance_rules[1].id )) - self.debug("VM ids: %s, %s" % (self.vm_1.id, self.vm_2.id)) - + self.debug("VM ids: %s, %s" % (self.vm_1.id, self.vm_2.id)) + self.assertIn( lb_instance_rules[0].id, [self.vm_1.id, self.vm_2.id], @@ -746,35 +774,35 @@ class TestLoadBalancingRule(cloudstackTestCase): ) try: self.debug( - "SSH into VM (IPaddress: %s) & NAT Rule (Public IP: %s)"% + "SSH into VM (IPaddress: %s) & NAT Rule (Public IP: %s)" % (self.vm_1.ipaddress, src_nat_ip_addr.ipaddress) ) - + ssh_1 = remoteSSHClient.remoteSSHClient( src_nat_ip_addr.ipaddress, self.services['lbrule']["publicport"], self.vm_1.username, self.vm_1.password ) - + # If Round Robin Algorithm is chosen, # each ssh command should alternate between VMs hostnames = [ssh_1.execute("hostname")[0]] - + except Exception as e: - self.fail("%s: SSH failed for VM with IP Address: %s" % + self.fail("%s: SSH failed for VM with IP Address: %s" % (e, src_nat_ip_addr.ipaddress)) time.sleep(self.services["lb_switch_wait"]) - + try: - self.debug("SSHing into IP address: %s after adding VMs (ID: %s , %s)" % + self.debug("SSHing into IP address: %s after adding VMs (ID: %s , %s)" % ( src_nat_ip_addr.ipaddress, self.vm_1.id, self.vm_2.id )) - + ssh_2 = remoteSSHClient.remoteSSHClient( src_nat_ip_addr.ipaddress, self.services['lbrule']["publicport"], @@ -784,7 +812,7 @@ class TestLoadBalancingRule(cloudstackTestCase): hostnames.append(ssh_2.execute("hostname")[0]) except Exception as e: - self.fail("%s: SSH failed for VM with IP Address: %s" % + self.fail("%s: SSH failed for VM with IP Address: %s" % (e, src_nat_ip_addr.ipaddress)) self.debug("Hostnames: %s" % str(hostnames)) @@ -802,23 +830,23 @@ class TestLoadBalancingRule(cloudstackTestCase): #SSH should pass till there is a last VM associated with LB rule lb_rule.remove(self.apiclient, [self.vm_2]) try: - self.debug("SSHing into IP address: %s after removing VM (ID: %s)" % + self.debug("SSHing into IP address: %s after removing VM (ID: %s)" % ( src_nat_ip_addr.ipaddress, self.vm_2.id )) - + ssh_1 = remoteSSHClient.remoteSSHClient( src_nat_ip_addr.ipaddress, self.services['lbrule']["publicport"], self.vm_1.username, self.vm_1.password ) - + hostnames.append(ssh_1.execute("hostname")[0]) - + except Exception as e: - self.fail("%s: SSH failed for VM with IP Address: %s" % + self.fail("%s: SSH failed for VM with IP Address: %s" % (e, src_nat_ip_addr.ipaddress)) self.assertIn( @@ -828,7 +856,7 @@ class TestLoadBalancingRule(cloudstackTestCase): ) lb_rule.remove(self.apiclient, [self.vm_1]) - + with self.assertRaises(Exception): self.debug("Removed all VMs, trying to SSH") ssh_1 = remoteSSHClient.remoteSSHClient( @@ -848,20 +876,20 @@ class TestLoadBalancingRule(cloudstackTestCase): #2. attempt to ssh twice on the load balanced IP #3. verify using the hostname of the VM that # round robin is indeed happening as expected - + # Check if VM is in Running state before creating LB rule vm_response = VirtualMachine.list( self.apiclient, account=self.account.account.name, domainid=self.account.account.domainid ) - + self.assertEqual( isinstance(vm_response, list), True, "Check list VM returns a valid list" ) - + self.assertNotEqual( len(vm_response), 0, @@ -873,7 +901,7 @@ class TestLoadBalancingRule(cloudstackTestCase): 'Running', "VM state should be Running before creating a NAT rule." ) - + #Create Load Balancer rule and assign VMs to rule lb_rule = LoadBalancerRule.create( self.apiclient, @@ -889,7 +917,7 @@ class TestLoadBalancingRule(cloudstackTestCase): self.apiclient, id=lb_rule.id ) - + self.assertEqual( isinstance(lb_rules, list), True, @@ -935,7 +963,7 @@ class TestLoadBalancingRule(cloudstackTestCase): "Check List Load Balancer instances Rules returns valid VM ID" ) try: - self.debug("SSHing into IP address: %s after adding VMs (ID: %s , %s)" % + self.debug("SSHing into IP address: %s after adding VMs (ID: %s , %s)" % ( self.non_src_nat_ip.ipaddress.ipaddress, self.vm_1.id, @@ -951,10 +979,10 @@ class TestLoadBalancingRule(cloudstackTestCase): # If Round Robin Algorithm is chosen, # each ssh command should alternate between VMs hostnames = [ssh_1.execute("hostname")[0]] - + time.sleep(self.services["lb_switch_wait"]) - - self.debug("SSHing again into IP address: %s with VMs (ID: %s , %s) added to LB rule" % + + self.debug("SSHing again into IP address: %s with VMs (ID: %s , %s) added to LB rule" % ( self.non_src_nat_ip.ipaddress.ipaddress, self.vm_1.id, @@ -966,7 +994,7 @@ class TestLoadBalancingRule(cloudstackTestCase): self.vm_1.username, self.vm_1.password ) - + hostnames.append(ssh_2.execute("hostname")[0]) self.debug("Hostnames after adding 2 VMs to LB rule: %s" % str(hostnames)) self.assertIn( @@ -982,8 +1010,8 @@ class TestLoadBalancingRule(cloudstackTestCase): #SSH should pass till there is a last VM associated with LB rule lb_rule.remove(self.apiclient, [self.vm_2]) - - self.debug("SSHing into IP address: %s after removing VM (ID: %s) from LB rule" % + + self.debug("SSHing into IP address: %s after removing VM (ID: %s) from LB rule" % ( self.non_src_nat_ip.ipaddress.ipaddress, self.vm_2.id @@ -994,11 +1022,11 @@ class TestLoadBalancingRule(cloudstackTestCase): self.vm_1.username, self.vm_1.password ) - + hostnames.append(ssh_1.execute("hostname")[0]) self.debug("Hostnames after removing VM2: %s" % str(hostnames)) except Exception as e: - self.fail("%s: SSH failed for VM with IP Address: %s" % + self.fail("%s: SSH failed for VM with IP Address: %s" % (e, self.non_src_nat_ip.ipaddress.ipaddress)) self.assertIn( @@ -1009,7 +1037,7 @@ class TestLoadBalancingRule(cloudstackTestCase): lb_rule.remove(self.apiclient, [self.vm_1]) with self.assertRaises(Exception): - self.fail("SSHing into IP address: %s after removing VM (ID: %s) from LB rule" % + self.fail("SSHing into IP address: %s after removing VM (ID: %s) from LB rule" % ( self.non_src_nat_ip.ipaddress.ipaddress, self.vm_1.id @@ -1060,7 +1088,7 @@ class TestRebootRouter(cloudstackTestCase): domainid=self.account.account.domainid, serviceofferingid=self.service_offering.id ) - + src_nat_ip_addrs = list_publicIP( self.apiclient, account=self.account.account.name, @@ -1070,7 +1098,7 @@ class TestRebootRouter(cloudstackTestCase): src_nat_ip_addr = src_nat_ip_addrs[0] except Exception as e: raise Exception("Warning: Exception during fetching source NAT: %s" % e) - + self.public_ip = PublicIPAddress.create( self.apiclient, self.vm_1.account, @@ -1078,6 +1106,15 @@ class TestRebootRouter(cloudstackTestCase): self.vm_1.domainid, self.services["server"] ) + # Open up firewall port for SSH + fw_rule = FireWallRule.create( + self.apiclient, + ipaddressid=self.public_ip.ipaddress.id, + protocol=self.services["lbrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["lbrule"]["publicport"], + endport=self.services["lbrule"]["publicport"] + ) lb_rule = LoadBalancerRule.create( self.apiclient, @@ -1120,44 +1157,44 @@ class TestRebootRouter(cloudstackTestCase): True, "Check list routers returns a valid list" ) - + router = routers[0] - + self.debug("Rebooting the router (ID: %s)" % router.id) - + cmd = rebootRouter.rebootRouterCmd() cmd.id = router.id self.apiclient.rebootRouter(cmd) - + # Poll listVM to ensure VM is stopped properly timeout = self.services["timeout"] - + while True: time.sleep(self.services["sleep"]) - + # Ensure that VM is in stopped state list_vm_response = list_virtual_machines( self.apiclient, id=self.vm_1.id ) - + if isinstance(list_vm_response, list): - + vm = list_vm_response[0] if vm.state == 'Running': self.debug("VM state: %s" % vm.state) break - - if timeout == 0: + + if timeout == 0: raise Exception( "Failed to start VM (ID: %s) in change service offering" % vm.id) - + timeout = timeout - 1 #we should be able to SSH after successful reboot try: self.debug("SSH into VM (ID : %s ) after reboot" % self.vm_1.id) - + remoteSSHClient.remoteSSHClient( self.nat_rule.ipaddress, self.services["natrule"]["publicport"], @@ -1258,20 +1295,30 @@ class TestAssignRemoveLB(cloudstackTestCase): "Check list response returns a valid list" ) self.non_src_nat_ip = src_nat_ip_addrs[0] - + + # Open up firewall port for SSH + fw_rule = FireWallRule.create( + self.apiclient, + ipaddressid=self.non_src_nat_ip.id, + protocol=self.services["lbrule"]["protocol"], + cidrlist=['0.0.0.0/0'], + startport=self.services["lbrule"]["publicport"], + endport=self.services["lbrule"]["publicport"] + ) + # Check if VM is in Running state before creating LB rule vm_response = VirtualMachine.list( self.apiclient, account=self.account.account.name, domainid=self.account.account.domainid ) - + self.assertEqual( isinstance(vm_response, list), True, "Check list VM returns a valid list" ) - + self.assertNotEqual( len(vm_response), 0, @@ -1283,7 +1330,7 @@ class TestAssignRemoveLB(cloudstackTestCase): 'Running', "VM state should be Running before creating a NAT rule." ) - + lb_rule = LoadBalancerRule.create( self.apiclient, self.services["lbrule"], @@ -1291,9 +1338,9 @@ class TestAssignRemoveLB(cloudstackTestCase): self.account.account.name ) lb_rule.assign(self.apiclient, [self.vm_1, self.vm_2]) - + try: - self.debug("SSHing into IP address: %s with VMs (ID: %s , %s) added to LB rule" % + self.debug("SSHing into IP address: %s with VMs (ID: %s , %s) added to LB rule" % ( self.non_src_nat_ip.ipaddress, self.vm_1.id, @@ -1307,11 +1354,11 @@ class TestAssignRemoveLB(cloudstackTestCase): self.vm_1.password ) except Exception as e: - self.fail("SSH failed for VM with IP: %s" % + self.fail("SSH failed for VM with IP: %s" % self.non_src_nat_ip.ipaddress) - + try: - self.debug("SSHing again into IP address: %s with VMs (ID: %s , %s) added to LB rule" % + self.debug("SSHing again into IP address: %s with VMs (ID: %s , %s) added to LB rule" % ( self.non_src_nat_ip.ipaddress, self.vm_1.id, @@ -1323,19 +1370,19 @@ class TestAssignRemoveLB(cloudstackTestCase): self.vm_2.username, self.vm_2.password ) - + # If Round Robin Algorithm is chosen, # each ssh command should alternate between VMs res_1 = ssh_1.execute("hostname")[0] self.debug(res_1) - + time.sleep(self.services["lb_switch_wait"]) - + res_2 = ssh_2.execute("hostname")[0] self.debug(res_2) except Exception as e: - self.fail("SSH failed for VM with IP: %s" % + self.fail("SSH failed for VM with IP: %s" % self.non_src_nat_ip.ipaddress) self.assertIn( @@ -1351,9 +1398,9 @@ class TestAssignRemoveLB(cloudstackTestCase): #Removing VM and assigning another VM to LB rule lb_rule.remove(self.apiclient, [self.vm_2]) - + try: - self.debug("SSHing again into IP address: %s with VM (ID: %s) added to LB rule" % + self.debug("SSHing again into IP address: %s with VM (ID: %s) added to LB rule" % ( self.non_src_nat_ip.ipaddress, self.vm_1.id, @@ -1364,14 +1411,14 @@ class TestAssignRemoveLB(cloudstackTestCase): self.services["lbrule"]["publicport"], self.vm_1.username, self.vm_1.password - ) + ) res_1 = ssh_1.execute("hostname")[0] self.debug(res_1) except Exception as e: - self.fail("SSH failed for VM with IP: %s" % + self.fail("SSH failed for VM with IP: %s" % self.non_src_nat_ip.ipaddress) - + self.assertIn( self.vm_1.name, res_1, @@ -1379,7 +1426,7 @@ class TestAssignRemoveLB(cloudstackTestCase): ) lb_rule.assign(self.apiclient, [self.vm_3]) - + try: ssh_1 = remoteSSHClient.remoteSSHClient( self.non_src_nat_ip.ipaddress, @@ -1393,19 +1440,19 @@ class TestAssignRemoveLB(cloudstackTestCase): self.vm_3.username, self.vm_3.password ) - + res_1 = ssh_1.execute("hostname")[0] self.debug(res_1) - + time.sleep(self.services["lb_switch_wait"]) - + res_3 = ssh_3.execute("hostname")[0] self.debug(res_3) except Exception as e: - self.fail("SSH failed for VM with IP: %s" % + self.fail("SSH failed for VM with IP: %s" % self.non_src_nat_ip.ipaddress) - + self.assertIn( self.vm_1.name, res_1, @@ -1478,7 +1525,7 @@ class TestReleaseIP(cloudstackTestCase): except Exception as e: raise Exception("Failed: During acquiring source NAT for account: %s" % self.account.account.name) - + self.nat_rule = NATRule.create( self.apiclient, self.virtual_machine, @@ -1502,21 +1549,21 @@ class TestReleaseIP(cloudstackTestCase): def test_releaseIP(self): """Test for Associate/Disassociate public IP address""" - + self.debug("Deleting Public IP : %s" % self.ip_addr.id) - + self.ip_address.delete(self.apiclient) - + # Sleep to ensure that deleted state is reflected in other calls time.sleep(self.services["sleep"]) - + # ListPublicIpAddresses should not list deleted Public IP address list_pub_ip_addr_resp = list_publicIP( self.apiclient, id=self.ip_addr.id ) self.debug("List Public IP response" + str(list_pub_ip_addr_resp)) - + self.assertEqual( list_pub_ip_addr_resp, None, @@ -1543,7 +1590,7 @@ class TestReleaseIP(cloudstackTestCase): id=self.lb_rule.id ) self.debug("List LB Rule response" + str(list_lb_rule)) - + self.assertEqual( list_lb_rule, None, @@ -1603,12 +1650,12 @@ class TestDeleteAccount(cloudstackTestCase): account=self.account.account.name, domainid=self.account.account.domainid ) - + try: src_nat_ip_addr = src_nat_ip_addrs[0] - + except Exception as e: - self.fail("SSH failed for VM with IP: %s" % + self.fail("SSH failed for VM with IP: %s" % src_nat_ip_addr.ipaddress) self.lb_rule = LoadBalancerRule.create( @@ -1618,7 +1665,7 @@ class TestDeleteAccount(cloudstackTestCase): self.account.account.name ) self.lb_rule.assign(self.apiclient, [self.vm_1]) - + self.nat_rule = NATRule.create( self.apiclient, self.vm_1, @@ -1666,14 +1713,14 @@ class TestDeleteAccount(cloudstackTestCase): "Check load balancing rule is properly deleted." ) except Exception as e: - + raise Exception( "Exception raised while fetching LB rules for account: %s" % self.account.account.name) # ListPortForwardingRules should not # list associated rules with deleted account try: - list_nat_reponse= list_nat_rules( + list_nat_reponse = list_nat_rules( self.apiclient, account=self.account.account.name, domainid=self.account.account.domainid @@ -1684,7 +1731,7 @@ class TestDeleteAccount(cloudstackTestCase): "Check load balancing rule is properly deleted." ) except Exception as e: - + raise Exception( "Exception raised while fetching NAT rules for account: %s" % self.account.account.name) @@ -1701,7 +1748,7 @@ class TestDeleteAccount(cloudstackTestCase): "Check routers are properly deleted." ) except Exception as e: - + raise Exception( "Exception raised while fetching routers for account: %s" % self.account.account.name) diff --git a/test/integration/smoke/test_routers.py b/test/integration/smoke/test_routers.py index 5937ddf00a3..79e6bbdb2ee 100644 --- a/test/integration/smoke/test_routers.py +++ b/test/integration/smoke/test_routers.py @@ -21,7 +21,6 @@ from marvin.remoteSSHClient import remoteSSHClient from integration.lib.utils import * from integration.lib.base import * from integration.lib.common import * - #Import System modules import time @@ -57,7 +56,7 @@ class Services: "username": "testuser", "password": "fr3sca", }, - "ostypeid":'5776c0d2-f331-42db-ba3a-29f1f8319bc9', + "ostypeid":'946b031b-0e10-4f4a-a3fc-d212ae2ea07f', "sleep": 60, "timeout": 10, "mode": 'advanced', #Networking mode: Basic, Advanced diff --git a/test/integration/smoke/test_snapshots.py b/test/integration/smoke/test_snapshots.py index 6b3ec704ff0..91e65a41a0d 100644 --- a/test/integration/smoke/test_snapshots.py +++ b/test/integration/smoke/test_snapshots.py @@ -17,10 +17,10 @@ import marvin from marvin.cloudstackTestCase import * from marvin.cloudstackAPI import * +from marvin.remoteSSHClient import remoteSSHClient from integration.lib.utils import * from integration.lib.base import * from integration.lib.common import * -from marvin.remoteSSHClient import remoteSSHClient class Services: @@ -1017,7 +1017,7 @@ class TestSnapshots(cloudstackTestCase): self.services["sub_lvl_dir2"], self.services["random_data"] ), - "sync", + "sync", ] for c in cmds: diff --git a/test/integration/smoke/test_templates.py b/test/integration/smoke/test_templates.py index 0dd873cd623..b17b93fda19 100644 --- a/test/integration/smoke/test_templates.py +++ b/test/integration/smoke/test_templates.py @@ -17,6 +17,7 @@ import marvin from marvin.cloudstackTestCase import * from marvin.cloudstackAPI import * +from marvin.remoteSSHClient import remoteSSHClient from integration.lib.utils import * from integration.lib.base import * from integration.lib.common import * @@ -69,12 +70,12 @@ class Services: "template_1": { "displaytext": "Cent OS Template", "name": "Cent OS Template", - "ostypeid": '5776c0d2-f331-42db-ba3a-29f1f8319bc9', + "ostypeid": '946b031b-0e10-4f4a-a3fc-d212ae2ea07f', }, "template_2": { "displaytext": "Public Template", "name": "Public template", - "ostypeid": '5776c0d2-f331-42db-ba3a-29f1f8319bc9', + "ostypeid": '946b031b-0e10-4f4a-a3fc-d212ae2ea07f', "isfeatured": True, "ispublic": True, "isextractable": True, @@ -88,7 +89,7 @@ class Services: "isextractable": False, "bootable": True, "passwordenabled": True, - "ostypeid": '5776c0d2-f331-42db-ba3a-29f1f8319bc9', + "ostypeid": '946b031b-0e10-4f4a-a3fc-d212ae2ea07f', "mode": 'advanced', # Networking mode: Advanced, basic "sleep": 30, diff --git a/test/integration/smoke/test_vm_life_cycle.py b/test/integration/smoke/test_vm_life_cycle.py index 618583fcdab..839124308e5 100644 --- a/test/integration/smoke/test_vm_life_cycle.py +++ b/test/integration/smoke/test_vm_life_cycle.py @@ -107,6 +107,11 @@ class Services: "ostypeid": '5776c0d2-f331-42db-ba3a-29f1f8319bc9', "mode": 'HTTP_DOWNLOAD', # Downloading existing ISO }, + "template": { + "displaytext": "Cent OS Template", + "name": "Cent OS Template", + "passwordenabled": True, + }, "diskdevice": '/dev/xvdd', # Disk device where ISO is attached to instance "mount_dir": "/mnt/tmp", @@ -186,20 +191,20 @@ class TestDeployVM(cloudstackTestCase): "Verify listVirtualMachines response for virtual machine: %s" \ % self.virtual_machine.id ) - + self.assertEqual( isinstance(list_vm_response, list), True, "Check list response returns a valid list" ) - + self.assertNotEqual( len(list_vm_response), 0, "Check VM available in List Virtual Machines" ) vm_response = list_vm_response[0] - + self.assertEqual( vm_response.id, @@ -323,7 +328,7 @@ class TestVMLifeCycle(cloudstackTestCase): self.apiclient, id=self.small_virtual_machine.id ) - + self.assertEqual( isinstance(list_vm_response, list), True, @@ -348,7 +353,7 @@ class TestVMLifeCycle(cloudstackTestCase): # Validate the following # 1. listVM command should return this VM.State # of this VM should be Running". - + self.debug("Starting VM - ID: %s" % self.virtual_machine.id) self.small_virtual_machine.start(self.apiclient) @@ -361,7 +366,7 @@ class TestVMLifeCycle(cloudstackTestCase): True, "Check list response returns a valid list" ) - + self.assertNotEqual( len(list_vm_response), 0, @@ -400,7 +405,7 @@ class TestVMLifeCycle(cloudstackTestCase): True, "Check list response returns a valid list" ) - + self.assertNotEqual( len(list_vm_response), 0, @@ -423,39 +428,39 @@ class TestVMLifeCycle(cloudstackTestCase): # this Vm matches the one specified for "Small" service offering. # 2. Using listVM command verify that this Vm # has Small service offering Id. - + self.debug("Stopping VM - ID: %s" % self.medium_virtual_machine.id) - + self.medium_virtual_machine.stop(self.apiclient) - + # Poll listVM to ensure VM is stopped properly timeout = self.services["timeout"] - + while True: time.sleep(self.services["sleep"]) - + # Ensure that VM is in stopped state list_vm_response = list_virtual_machines( self.apiclient, id=self.medium_virtual_machine.id ) - + if isinstance(list_vm_response, list): - + vm = list_vm_response[0] if vm.state == 'Stopped': self.debug("VM state: %s" % vm.state) break - - if timeout == 0: + + if timeout == 0: raise Exception( "Failed to stop VM (ID: %s) in change service offering" % vm.id) - + timeout = timeout - 1 - - self.debug("Change Service offering VM - ID: %s" % + + self.debug("Change Service offering VM - ID: %s" % self.medium_virtual_machine.id) - + cmd = changeServiceForVirtualMachine.changeServiceForVirtualMachineCmd() cmd.id = self.medium_virtual_machine.id cmd.serviceofferingid = self.small_offering.id @@ -463,32 +468,32 @@ class TestVMLifeCycle(cloudstackTestCase): self.debug("Starting VM - ID: %s" % self.medium_virtual_machine.id) self.medium_virtual_machine.start(self.apiclient) - + # Poll listVM to ensure VM is started properly timeout = self.services["timeout"] - + while True: time.sleep(self.services["sleep"]) - + # Ensure that VM is in running state list_vm_response = list_virtual_machines( self.apiclient, id=self.medium_virtual_machine.id ) - + if isinstance(list_vm_response, list): - + vm = list_vm_response[0] if vm.state == 'Running': self.debug("VM state: %s" % vm.state) break - - if timeout == 0: + + if timeout == 0: raise Exception( "Failed to start VM (ID: %s) after changing service offering" % vm.id) - + timeout = timeout - 1 - + try: ssh = self.medium_virtual_machine.get_ssh_client() except Exception as e: @@ -506,7 +511,7 @@ class TestVMLifeCycle(cloudstackTestCase): meminfo = ssh.execute("cat /proc/meminfo") #MemTotal: 1017464 kB total_mem = [i for i in meminfo if "MemTotal" in i][0].split()[1] - + self.debug( "CPU count: %s, CPU Speed: %s, Mem Info: %s" % ( cpu_cnt, @@ -539,76 +544,76 @@ class TestVMLifeCycle(cloudstackTestCase): # this Vm matches the one specified for "Medium" service offering. # 2. Using listVM command verify that this Vm # has Medium service offering Id. - + self.debug("Stopping VM - ID: %s" % self.small_virtual_machine.id) self.small_virtual_machine.stop(self.apiclient) - + # Poll listVM to ensure VM is stopped properly timeout = self.services["timeout"] - + while True: time.sleep(self.services["sleep"]) - + # Ensure that VM is in stopped state list_vm_response = list_virtual_machines( self.apiclient, id=self.small_virtual_machine.id ) - + if isinstance(list_vm_response, list): - + vm = list_vm_response[0] if vm.state == 'Stopped': self.debug("VM state: %s" % vm.state) break - - if timeout == 0: + + if timeout == 0: raise Exception( "Failed to stop VM (ID: %s) in change service offering" % vm.id) - + timeout = timeout - 1 - - self.debug("Change service offering VM - ID: %s" % + + self.debug("Change service offering VM - ID: %s" % self.small_virtual_machine.id) - + cmd = changeServiceForVirtualMachine.changeServiceForVirtualMachineCmd() cmd.id = self.small_virtual_machine.id cmd.serviceofferingid = self.medium_offering.id self.apiclient.changeServiceForVirtualMachine(cmd) - + self.debug("Starting VM - ID: %s" % self.small_virtual_machine.id) self.small_virtual_machine.start(self.apiclient) - + # Poll listVM to ensure VM is started properly timeout = self.services["timeout"] - + while True: time.sleep(self.services["sleep"]) - + # Ensure that VM is in running state list_vm_response = list_virtual_machines( self.apiclient, id=self.small_virtual_machine.id ) - + if isinstance(list_vm_response, list): - + vm = list_vm_response[0] if vm.state == 'Running': self.debug("VM state: %s" % vm.state) break - - if timeout == 0: + + if timeout == 0: raise Exception( "Failed to start VM (ID: %s) after changing service offering" % vm.id) - + timeout = timeout - 1 - + list_vm_response = list_virtual_machines( self.apiclient, id=self.small_virtual_machine.id ) - + try: ssh_client = self.small_virtual_machine.get_ssh_client() except Exception as e: @@ -616,7 +621,7 @@ class TestVMLifeCycle(cloudstackTestCase): "SSH Access failed for %s: %s" % \ (self.small_virtual_machine.ipaddress, e) ) - + cpuinfo = ssh_client.execute("cat /proc/cpuinfo") cpu_cnt = len([i for i in cpuinfo if "processor" in i]) @@ -626,7 +631,7 @@ class TestVMLifeCycle(cloudstackTestCase): meminfo = ssh_client.execute("cat /proc/meminfo") #MemTotal: 1017464 kB total_mem = [i for i in meminfo if "MemTotal" in i][0].split()[1] - + self.debug( "CPU count: %s, CPU Speed: %s, Mem Info: %s" % ( cpu_cnt, @@ -644,7 +649,7 @@ class TestVMLifeCycle(cloudstackTestCase): self.medium_offering.cpuspeed, "Check CPU Speed for medium offering" ) - + self.assertAlmostEqual( int(total_mem) / 1024, # In MBs self.medium_offering.memory, @@ -660,7 +665,7 @@ class TestVMLifeCycle(cloudstackTestCase): # 1. Should not be able to login to the VM. # 2. listVM command should return this VM.State # of this VM should be "Destroyed". - + self.debug("Destroy VM - ID: %s" % self.small_virtual_machine.id) self.small_virtual_machine.delete(self.apiclient) @@ -673,7 +678,7 @@ class TestVMLifeCycle(cloudstackTestCase): True, "Check list response returns a valid list" ) - + self.assertNotEqual( len(list_vm_response), 0, @@ -695,9 +700,9 @@ class TestVMLifeCycle(cloudstackTestCase): # 1. listVM command should return this VM. # State of this VM should be "Stopped". # 2. We should be able to Start this VM successfully. - + self.debug("Recovering VM - ID: %s" % self.small_virtual_machine.id) - + cmd = recoverVirtualMachine.recoverVirtualMachineCmd() cmd.id = self.small_virtual_machine.id self.apiclient.recoverVirtualMachine(cmd) @@ -711,7 +716,7 @@ class TestVMLifeCycle(cloudstackTestCase): True, "Check list response returns a valid list" ) - + self.assertNotEqual( len(list_vm_response), 0, @@ -734,21 +739,21 @@ class TestVMLifeCycle(cloudstackTestCase): # 2. listVM command should return this VM.State of this VM # should be "Running" and the host should be the host # to which the VM was migrated to - + hosts = Host.list( - self.apiclient, + self.apiclient, zoneid=self.medium_virtual_machine.zoneid, type='Routing' ) - + self.assertEqual( - isinstance(hosts, list), - True, + isinstance(hosts, list), + True, "Check the number of hosts in the zone" ) self.assertEqual( - len(hosts), - 2, + len(hosts), + 2, "Atleast 2 hosts should be present in a zone for VM migration" ) @@ -757,12 +762,12 @@ class TestVMLifeCycle(cloudstackTestCase): host = hosts[1] else: host = hosts[0] - + self.debug("Migrating VM-ID: %s to Host: %s" % ( self.medium_virtual_machine.id, host.id )) - + cmd = migrateVirtualMachine.migrateVirtualMachineCmd() cmd.hostid = host.id cmd.virtualmachineid = self.medium_virtual_machine.id @@ -777,7 +782,7 @@ class TestVMLifeCycle(cloudstackTestCase): True, "Check list response returns a valid list" ) - + self.assertNotEqual( list_vm_response, None, @@ -804,9 +809,9 @@ class TestVMLifeCycle(cloudstackTestCase): """ # Validate the following # 1. listVM command should NOT return this VM any more. - + self.debug("Expunge VM-ID: %s" % self.small_virtual_machine.id) - + cmd = destroyVirtualMachine.destroyVirtualMachineCmd() cmd.id = self.small_virtual_machine.id self.apiclient.destroyVirtualMachine(cmd) @@ -848,14 +853,14 @@ class TestVMLifeCycle(cloudstackTestCase): account=self.account.account.name, domainid=self.account.account.domainid ) - + self.debug("Successfully created ISO with ID: %s" % iso.id) try: iso.download(self.apiclient) except Exception as e: self.fail("Exception while downloading ISO %s: %s"\ % (iso.id, e)) - + self.debug("Attach ISO with ID: %s to VM ID: %s" % ( iso.id, self.virtual_machine.id @@ -865,10 +870,10 @@ class TestVMLifeCycle(cloudstackTestCase): cmd.id = iso.id cmd.virtualmachineid = self.virtual_machine.id self.apiclient.attachIso(cmd) - + try: ssh_client = self.virtual_machine.get_ssh_client() - + cmds = [ "mkdir -p %s" % self.services["mount_dir"], "mount -rt iso9660 %s %s" \ @@ -877,7 +882,7 @@ class TestVMLifeCycle(cloudstackTestCase): self.services["mount_dir"] ), ] - + for c in cmds: res = ssh_client.execute(c) @@ -888,9 +893,9 @@ class TestVMLifeCycle(cloudstackTestCase): #Disk /dev/xvdd: 4393 MB, 4393723904 bytes except Exception as e: - self.fail("SSH failed for virtual machine: %s - %s" % + self.fail("SSH failed for virtual machine: %s - %s" % (self.virtual_machine.ipaddress, e)) - + # Res may contain more than one strings depending on environment # Split strings to form new list which is used for assertion on ISO size result = [] @@ -919,23 +924,23 @@ class TestVMLifeCycle(cloudstackTestCase): #Unmount ISO command = "umount %s" % self.services["mount_dir"] ssh_client.execute(command) - + except Exception as e: - self.fail("SSH failed for virtual machine: %s - %s" % + self.fail("SSH failed for virtual machine: %s - %s" % (self.virtual_machine.ipaddress, e)) - + #Detach from VM cmd = detachIso.detachIsoCmd() cmd.virtualmachineid = self.virtual_machine.id self.apiclient.detachIso(cmd) - + try: res = ssh_client.execute(c) - + except Exception as e: - self.fail("SSH failed for virtual machine: %s - %s" % + self.fail("SSH failed for virtual machine: %s - %s" % (self.virtual_machine.ipaddress, e)) - + # Check if ISO is properly detached from VM (using fdisk) result = self.services["diskdevice"] in str(res) @@ -945,3 +950,174 @@ class TestVMLifeCycle(cloudstackTestCase): "Check if ISO is detached from virtual machine" ) return + +@unittest.skip("Additional test") +class TestVMPasswordEnabled(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.api_client = super( + TestVMPasswordEnabled, + cls + ).getClsTestClient().getApiClient() + cls.services = Services().services + + # Get Zone, Domain and templates + domain = get_domain(cls.api_client, cls.services) + zone = get_zone(cls.api_client, cls.services) + template = get_template( + cls.api_client, + zone.id, + cls.services["ostypeid"] + ) + # Set Zones and disk offerings + cls.services["small"]["zoneid"] = zone.id + cls.services["small"]["template"] = template.id + + # Create VMs, NAT Rules etc + cls.account = Account.create( + cls.api_client, + cls.services["account"], + domainid=domain.id + ) + + cls.small_offering = ServiceOffering.create( + cls.api_client, + cls.services["service_offerings"]["small"] + ) + + cls.virtual_machine = VirtualMachine.create( + cls.api_client, + cls.services["small"], + accountid=cls.account.account.name, + domainid=cls.account.account.domainid, + serviceofferingid=cls.small_offering.id, + mode=cls.services["mode"] + ) + #Stop virtual machine + cls.virtual_machine.stop(cls.api_client) + + # Poll listVM to ensure VM is stopped properly + timeout = cls.services["timeout"] + while True: + time.sleep(cls.services["sleep"]) + + # Ensure that VM is in stopped state + list_vm_response = list_virtual_machines( + cls.api_client, + id=cls.virtual_machine.id + ) + + if isinstance(list_vm_response, list): + + vm = list_vm_response[0] + if vm.state == 'Stopped': + break + + if timeout == 0: + raise Exception( + "Failed to stop VM (ID: %s) in change service offering" % + vm.id) + + timeout = timeout - 1 + + list_volume = list_volumes( + cls.api_client, + virtualmachineid=cls.virtual_machine.id, + type='ROOT', + listall=True + ) + if isinstance(list_volume, list): + cls.volume = list_volume[0] + else: + raise Exception( + "Exception: Unable to find root volume foe VM: %s" % + cls.virtual_machine.id) + + cls.services["template"]["ostypeid"] = cls.services["ostypeid"] + #Create templates for Edit, Delete & update permissions testcases + cls.pw_enabled_template = Template.create( + cls.api_client, + cls.services["template"], + cls.volume.id, + account=cls.account.account.name, + domainid=cls.account.account.domainid + ) + # Delete the VM - No longer needed + cls.virtual_machine.delete(cls.api_client) + cls.services["small"]["template"] = cls.pw_enabled_template.id + + cls.vm = VirtualMachine.create( + cls.api_client, + cls.services["small"], + accountid=cls.account.account.name, + domainid=cls.account.account.domainid, + serviceofferingid=cls.small_offering.id, + mode=cls.services["mode"] + ) + cls._cleanup = [ + cls.small_offering, + cls.pw_enabled_template, + cls.account + ] + + @classmethod + def tearDownClass(cls): + # Cleanup VMs, templates etc. + cleanup_resources(cls.api_client, cls._cleanup) + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + + def tearDown(self): + #Clean up, terminate the created instances + cleanup_resources(self.apiclient, self.cleanup) + return + + def test_11_get_vm_password(self): + """Test get VM password for password enabled template""" + + # Validate the following + # 1. Create an account + # 2. Deploy VM with default service offering and "password enabled" + # template. Vm should be in running state. + # 3. Stop VM deployed in step 2 + # 4. Reset VM password. SSH with new password should be successful + + self.debug("Stopping VM: %s" % self.vm.name) + self.vm.stop(self.apiclient) + + # Sleep to ensure VM is stopped properly + time.sleep(self.services["sleep"]) + + self.debug("Resetting VM password for VM: %s" % self.vm.name) + password = self.vm.resetPassword(self.apiclient) + self.debug("Password reset to: %s" % password) + + self.debug("Starting VM to verify new password..") + self.vm.start(self.apiclient) + self.debug("VM - %s stated!" % self.vm.name) + + vms = VirtualMachine.list(self.apiclient, id=self.vm.id, listall=True) + self.assertEqual( + isinstance(vms, list), + True, + "List VMs should retun valid response for VM: %s" % self.vm.name + ) + virtual_machine = vms[0] + + self.assertEqual( + virtual_machine.state, + "Running", + "VM state should be running" + ) + try: + self.debug("SSHing into VM: %s" % self.vm.ssh_ip) + self.vm.password = password + ssh = self.vm.get_ssh_client() + except Exception as e: + self.fail("SSH into VM: %s failed" % self.vm.ssh_ip) + return diff --git a/test/integration/smoke/test_volumes.py b/test/integration/smoke/test_volumes.py index 2b3ec594093..f6b5e79db90 100644 --- a/test/integration/smoke/test_volumes.py +++ b/test/integration/smoke/test_volumes.py @@ -17,10 +17,10 @@ import marvin from marvin.cloudstackTestCase import * from marvin.cloudstackAPI import * +from marvin.remoteSSHClient import remoteSSHClient from integration.lib.utils import * from integration.lib.base import * from integration.lib.common import * -from marvin.remoteSSHClient import remoteSSHClient #Import System modules import os import urllib @@ -70,7 +70,7 @@ class Services: "publicport": 22, "protocol": 'TCP', "diskdevice": "/dev/xvdb", - "ostypeid": '5776c0d2-f331-42db-ba3a-29f1f8319bc9', + "ostypeid": '946b031b-0e10-4f4a-a3fc-d212ae2ea07f', "mode": 'advanced', "sleep": 60, "timeout": 10, @@ -514,4 +514,4 @@ class TestVolumes(cloudstackTestCase): None, "Check if volume exists in ListVolumes" ) - return \ No newline at end of file + return From 895c361dca3d05644d76e771b880f6e2885210d5 Mon Sep 17 00:00:00 2001 From: Murali reddy Date: Tue, 22 May 2012 20:31:22 +0530 Subject: [PATCH 04/14] bug CS-13897: DB upgrade for external Firewall/external LB devices status CS-13897: resolved fixed --- .../cloud/upgrade/dao/Upgrade302to303.java | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) diff --git a/server/src/com/cloud/upgrade/dao/Upgrade302to303.java b/server/src/com/cloud/upgrade/dao/Upgrade302to303.java index 2a694c29127..ce7c46a5953 100644 --- a/server/src/com/cloud/upgrade/dao/Upgrade302to303.java +++ b/server/src/com/cloud/upgrade/dao/Upgrade302to303.java @@ -17,9 +17,14 @@ package com.cloud.upgrade.dao; */ import java.io.File; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.UUID; import org.apache.log4j.Logger; // +import com.cloud.dc.DataCenter.NetworkType; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; @@ -53,6 +58,195 @@ public class Upgrade302to303 implements DbUpgrade { @Override public void performDataMigration(Connection conn) { + setupExternalNetworkDevices(conn); + } + + private void setupExternalNetworkDevices(Connection conn) { + PreparedStatement dcSearchStmt, pNetworkStmt, devicesStmt = null; + ResultSet dcResults, pNetworksResults, devicesResult = null; + + try { + dcSearchStmt = conn.prepareStatement("SELECT id, networktype FROM `cloud`.`data_center`"); + dcResults = dcSearchStmt.executeQuery(); + while (dcResults.next()) { + long zoneId = dcResults.getLong(1); + long f5HostId = 0; + long srxHostId = 0; + + String networkType = dcResults.getString(2); + if (NetworkType.Advanced.toString().equalsIgnoreCase(networkType)) { + + devicesStmt = conn.prepareStatement("SELECT id, type FROM host WHERE data_center_id=? AND type = 'ExternalLoadBalancer' OR type = 'ExternalFirewall'"); + devicesStmt.setLong(1, zoneId); + devicesResult = devicesStmt.executeQuery(); + + while (devicesResult.next()) { + String device = devicesResult.getString(2); + if (device.equals("ExternalLoadBalancer")) { + f5HostId = devicesResult.getLong(1); + } else if (device.equals("ExternalFirewall")) { + srxHostId = devicesResult.getLong(1); + } + } + + // check if the deployment had F5 and SRX devices + if (f5HostId != 0 && srxHostId != 0) { + pNetworkStmt = conn.prepareStatement("SELECT id FROM `cloud`.`physical_network` where data_center_id=?"); + pNetworkStmt.setLong(1, zoneId); + pNetworksResults = pNetworkStmt.executeQuery(); + if (pNetworksResults.first()) { + long physicalNetworkId = pNetworksResults.getLong(1); + + // add F5BigIP provider and provider instance to physical network + addF5ServiceProvider(conn, physicalNetworkId, zoneId); + addF5LoadBalancer(conn, f5HostId, physicalNetworkId); + + // add SRX provider and provider instance to physical network + addSrxServiceProvider(conn, physicalNetworkId, zoneId); + addSrxFirewall(conn, srxHostId, physicalNetworkId); + } + } + } + } + if (dcResults != null) { + try { + dcResults.close(); + } catch (SQLException e) { + } + } + if (dcSearchStmt != null) { + try { + dcSearchStmt.close(); + } catch (SQLException e) { + } + } + } catch (SQLException e) { + throw new CloudRuntimeException("Exception while adding PhysicalNetworks", e); + } finally { + + } + } + + private void addF5LoadBalancer(Connection conn, long hostId, long physicalNetworkId){ + // add traffic types + PreparedStatement pstmtUpdate = null; + try{ + s_logger.debug("Adding F5 Big IP load balancer with host id " + hostId); + String insertF5 = "INSERT INTO `cloud`.`external_load_balancer_devices` (physical_network_id, host_id, provider_name, " + + "device_name, capacity, is_dedicated, device_state, allocation_state, is_inline, is_managed, uuid) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + pstmtUpdate = conn.prepareStatement(insertF5); + pstmtUpdate.setLong(1, physicalNetworkId); + pstmtUpdate.setLong(2, hostId); + pstmtUpdate.setString(3, "F5BigIp"); + pstmtUpdate.setString(4, "F5BigIp"); + pstmtUpdate.setLong(5, 0); + pstmtUpdate.setBoolean(6, false); + pstmtUpdate.setString(7, "Enabled"); + pstmtUpdate.setString(8, "Shared"); + pstmtUpdate.setBoolean(9, false); + pstmtUpdate.setBoolean(10, false); + pstmtUpdate.setString(11, UUID.randomUUID().toString()); + pstmtUpdate.executeUpdate(); + pstmtUpdate.close(); + }catch (SQLException e) { + throw new CloudRuntimeException("Exception while adding F5 load balancer due to", e); + } finally { + if (pstmtUpdate != null) { + try { + pstmtUpdate.close(); + } catch (SQLException e) { + } + } + } + } + + private void addSrxFirewall(Connection conn, long hostId, long physicalNetworkId){ + // add traffic types + PreparedStatement pstmtUpdate = null; + try{ + s_logger.debug("Adding SRX firewall device with host id " + hostId); + String insertSrx = "INSERT INTO `cloud`.`external_firewall_devices` (physical_network_id, host_id, provider_name, " + + "device_name, capacity, is_dedicated, device_state, allocation_state, uuid) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + pstmtUpdate = conn.prepareStatement(insertSrx); + pstmtUpdate.setLong(1, physicalNetworkId); + pstmtUpdate.setLong(2, hostId); + pstmtUpdate.setString(3, "JuniperSRX"); + pstmtUpdate.setString(4, "JuniperSRX"); + pstmtUpdate.setLong(5, 0); + pstmtUpdate.setBoolean(6, false); + pstmtUpdate.setString(7, "Enabled"); + pstmtUpdate.setString(8, "Shared"); + pstmtUpdate.setString(9, UUID.randomUUID().toString()); + pstmtUpdate.executeUpdate(); + pstmtUpdate.close(); + }catch (SQLException e) { + throw new CloudRuntimeException("Exception while adding F5 load balancer due to", e); + } finally { + if (pstmtUpdate != null) { + try { + pstmtUpdate.close(); + } catch (SQLException e) { + } + } + } + } + + private void addF5ServiceProvider(Connection conn, long physicalNetworkId, long zoneId){ + PreparedStatement pstmtUpdate = null; + try{ + // add physical network service provider - F5BigIp + s_logger.debug("Adding PhysicalNetworkServiceProvider F5BigIp"); + String insertPNSP = "INSERT INTO `cloud`.`physical_network_service_providers` (`uuid`, `physical_network_id` , `provider_name`, `state` ," + + "`destination_physical_network_id`, `vpn_service_provided`, `dhcp_service_provided`, `dns_service_provided`, `gateway_service_provided`," + + "`firewall_service_provided`, `source_nat_service_provided`, `load_balance_service_provided`, `static_nat_service_provided`," + + "`port_forwarding_service_provided`, `user_data_service_provided`, `security_group_service_provided`) VALUES (?,?,?,?,0,0,0,0,0,0,0,1,0,0,0,0)"; + + pstmtUpdate = conn.prepareStatement(insertPNSP); + pstmtUpdate.setString(1, UUID.randomUUID().toString()); + pstmtUpdate.setLong(2, physicalNetworkId); + pstmtUpdate.setString(3, "F5BigIp"); + pstmtUpdate.setString(4, "Enabled"); + pstmtUpdate.executeUpdate(); + pstmtUpdate.close(); + }catch (SQLException e) { + throw new CloudRuntimeException("Exception while adding PhysicalNetworkServiceProvider F5BigIp ", e); + } finally { + if (pstmtUpdate != null) { + try { + pstmtUpdate.close(); + } catch (SQLException e) { + } + } + } + } + + private void addSrxServiceProvider(Connection conn, long physicalNetworkId, long zoneId){ + PreparedStatement pstmtUpdate = null; + try{ + // add physical network service provider - JuniperSRX + s_logger.debug("Adding PhysicalNetworkServiceProvider JuniperSRX"); + String insertPNSP = "INSERT INTO `cloud`.`physical_network_service_providers` (`uuid`, `physical_network_id` , `provider_name`, `state` ," + + "`destination_physical_network_id`, `vpn_service_provided`, `dhcp_service_provided`, `dns_service_provided`, `gateway_service_provided`," + + "`firewall_service_provided`, `source_nat_service_provided`, `load_balance_service_provided`, `static_nat_service_provided`," + + "`port_forwarding_service_provided`, `user_data_service_provided`, `security_group_service_provided`) VALUES (?,?,?,?,0,0,0,0,1,1,1,0,1,1,0,0)"; + + pstmtUpdate = conn.prepareStatement(insertPNSP); + pstmtUpdate.setString(1, UUID.randomUUID().toString()); + pstmtUpdate.setLong(2, physicalNetworkId); + pstmtUpdate.setString(3, "JuniperSRX"); + pstmtUpdate.setString(4, "Enabled"); + pstmtUpdate.executeUpdate(); + pstmtUpdate.close(); + }catch (SQLException e) { + throw new CloudRuntimeException("Exception while adding PhysicalNetworkServiceProvider JuniperSRX ", e); + } finally { + if (pstmtUpdate != null) { + try { + pstmtUpdate.close(); + } catch (SQLException e) { + } + } + } } @Override From b041f4188104ca052a5f1ff09da0242127064cdb Mon Sep 17 00:00:00 2001 From: Devdeep Singh Date: Tue, 22 May 2012 21:37:16 +0530 Subject: [PATCH 05/14] CS-9919: Adding helper routines to query details of a port profile and associated policy maps. Also updating the error message logs. --- .../utils/cisco/n1kv/vsm/NetconfHelper.java | 29 +++- .../cloud/utils/cisco/n1kv/vsm/PolicyMap.java | 15 +++ .../utils/cisco/n1kv/vsm/PortProfile.java | 29 ++++ .../utils/cisco/n1kv/vsm/VsmCommand.java | 65 ++++++--- .../utils/cisco/n1kv/vsm/VsmOkResponse.java | 1 + .../cisco/n1kv/vsm/VsmPolicyMapResponse.java | 66 +++++++++ .../n1kv/vsm/VsmPortProfileResponse.java | 127 +++++++++++++++++- .../utils/cisco/n1kv/vsm/VsmResponse.java | 4 +- 8 files changed, 313 insertions(+), 23 deletions(-) create mode 100644 utils/src/com/cloud/utils/cisco/n1kv/vsm/PolicyMap.java create mode 100644 utils/src/com/cloud/utils/cisco/n1kv/vsm/PortProfile.java create mode 100644 utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmPolicyMapResponse.java diff --git a/utils/src/com/cloud/utils/cisco/n1kv/vsm/NetconfHelper.java b/utils/src/com/cloud/utils/cisco/n1kv/vsm/NetconfHelper.java index 9ed83c7356c..0ad368a4dfe 100644 --- a/utils/src/com/cloud/utils/cisco/n1kv/vsm/NetconfHelper.java +++ b/utils/src/com/cloud/utils/cisco/n1kv/vsm/NetconfHelper.java @@ -125,7 +125,7 @@ public class NetconfHelper { public void addPolicyMap(String name, int averageRate, int maxRate, int burstRate) throws CloudRuntimeException { - String command = VsmCommand.getPolicyMap(name, averageRate, maxRate, burstRate); + String command = VsmCommand.getAddPolicyMap(name, averageRate, maxRate, burstRate); if (command != null) { command = command.concat(SSH_NETCONF_TERMINATOR); send(command); @@ -180,18 +180,39 @@ public class NetconfHelper { } } - public void getPortProfileByName(String name) throws CloudRuntimeException { + public PortProfile getPortProfileByName(String name) throws CloudRuntimeException { String command = VsmCommand.getPortProfile(name); if (command != null) { command = command.concat(SSH_NETCONF_TERMINATOR); send(command); // parse the rpc reply. - VsmPortProfileResponse response = new VsmPortProfileResponse(receive().trim()); + String received = receive(); + VsmPortProfileResponse response = new VsmPortProfileResponse(received.trim()); if (!response.isResponseOk()) { throw new CloudRuntimeException("Error response while getting the port profile details."); + } else { + return response.getPortProfile(); } } else { - throw new CloudRuntimeException("Error generating rpc request for removing policy map."); + throw new CloudRuntimeException("Error generating rpc request for getting port profile."); + } + } + + public PolicyMap getPolicyMapByName(String name) throws CloudRuntimeException { + String command = VsmCommand.getPolicyMap(name); + if (command != null) { + command = command.concat(SSH_NETCONF_TERMINATOR); + send(command); + // parse the rpc reply. + String received = receive(); + VsmPolicyMapResponse response = new VsmPolicyMapResponse(received.trim()); + if (!response.isResponseOk()) { + throw new CloudRuntimeException("Error response while getting the port profile details."); + } else { + return response.getPolicyMap(); + } + } else { + throw new CloudRuntimeException("Error generating rpc request for getting policy map."); } } diff --git a/utils/src/com/cloud/utils/cisco/n1kv/vsm/PolicyMap.java b/utils/src/com/cloud/utils/cisco/n1kv/vsm/PolicyMap.java new file mode 100644 index 00000000000..08b552258b3 --- /dev/null +++ b/utils/src/com/cloud/utils/cisco/n1kv/vsm/PolicyMap.java @@ -0,0 +1,15 @@ +package com.cloud.utils.cisco.n1kv.vsm; + +public class PolicyMap { + public String policyMapName; + public int committedRate; + public int burstRate; + public int peakRate; + + PolicyMap() { + policyMapName = null; + committedRate = 0; + burstRate = 0; + peakRate = 0; + } +} diff --git a/utils/src/com/cloud/utils/cisco/n1kv/vsm/PortProfile.java b/utils/src/com/cloud/utils/cisco/n1kv/vsm/PortProfile.java new file mode 100644 index 00000000000..10fc6f13fdd --- /dev/null +++ b/utils/src/com/cloud/utils/cisco/n1kv/vsm/PortProfile.java @@ -0,0 +1,29 @@ +package com.cloud.utils.cisco.n1kv.vsm; + +import com.cloud.utils.cisco.n1kv.vsm.VsmCommand.BindingType; +import com.cloud.utils.cisco.n1kv.vsm.VsmCommand.PortProfileType; +import com.cloud.utils.cisco.n1kv.vsm.VsmCommand.SwitchPortMode; + +public class PortProfile { + public PortProfileType type; + public SwitchPortMode mode; + public BindingType binding; + public String profileName; + public String inputPolicyMap; + public String outputPolicyMap; + public String vlan; + public boolean status; + public int maxPorts; + + PortProfile() { + profileName = null; + inputPolicyMap = null; + outputPolicyMap = null; + vlan = null; + status = false; + maxPorts = 32; + type = PortProfileType.none; + mode = SwitchPortMode.none; + binding = BindingType.none; + } +} diff --git a/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmCommand.java b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmCommand.java index 444e6e07938..d510d6d0676 100644 --- a/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmCommand.java +++ b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmCommand.java @@ -79,10 +79,10 @@ public class VsmCommand { return serialize(domImpl, doc); } catch (ParserConfigurationException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating add port profile message : " + e.getMessage()); return null; } catch (DOMException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating add port profile message : " + e.getMessage()); return null; } } @@ -113,10 +113,10 @@ public class VsmCommand { return serialize(domImpl, doc); } catch (ParserConfigurationException e) { - s_logger.error("Error while creating update message : " + e.getMessage()); + s_logger.error("Error while creating update port profile message : " + e.getMessage()); return null; } catch (DOMException e) { - s_logger.error("Error while creating update message : " + e.getMessage()); + s_logger.error("Error while creating update port profile message : " + e.getMessage()); return null; } } @@ -146,15 +146,15 @@ public class VsmCommand { return serialize(domImpl, doc); } catch (ParserConfigurationException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating delete port profile message : " + e.getMessage()); return null; } catch (DOMException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating delete port profile message : " + e.getMessage()); return null; } } - public static String getPolicyMap(String name, int averageRate, int maxRate, int burstRate) { + public static String getAddPolicyMap(String name, int averageRate, int maxRate, int burstRate) { try { // Create the document and root element. DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); @@ -179,10 +179,10 @@ public class VsmCommand { return serialize(domImpl, doc); } catch (ParserConfigurationException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating policy map message : " + e.getMessage()); return null; } catch (DOMException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating policy map message : " + e.getMessage()); return null; } } @@ -212,10 +212,10 @@ public class VsmCommand { return serialize(domImpl, doc); } catch (ParserConfigurationException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating delete policy map message : " + e.getMessage()); return null; } catch (DOMException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating delete policy map message : " + e.getMessage()); return null; } } @@ -245,10 +245,10 @@ public class VsmCommand { return serialize(domImpl, doc); } catch (ParserConfigurationException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating attach/detach service policy message : " + e.getMessage()); return null; } catch (DOMException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating attach/detach service policy message : " + e.getMessage()); return null; } } @@ -282,10 +282,43 @@ public class VsmCommand { return serialize(domImpl, doc); } catch (ParserConfigurationException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating the message to get port profile details: " + e.getMessage()); return null; } catch (DOMException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating the message to get port profile details: " + e.getMessage()); + return null; + } + } + + public static String getPolicyMap(String name) { + try { + DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + DOMImplementation domImpl = docBuilder.getDOMImplementation(); + Document doc = createDocument(domImpl); + + Element get = doc.createElement("nf:get"); + doc.getDocumentElement().appendChild(get); + + Element filter = doc.createElement("nf:filter"); + filter.setAttribute("type", "subtree"); + get.appendChild(filter); + + // Create the show port-profile name command. + Element show = doc.createElement("show"); + filter.appendChild(show); + Element policyMap = doc.createElement("policy-map"); + show.appendChild(policyMap); + Element nameNode = doc.createElement("name"); + nameNode.setTextContent(name); + policyMap.appendChild(nameNode); + + return serialize(domImpl, doc); + } catch (ParserConfigurationException e) { + s_logger.error("Error while creating the message to get policy map details : " + e.getMessage()); + return null; + } catch (DOMException e) { + s_logger.error("Error while creating the message to get policy map details : " + e.getMessage()); return null; } } @@ -312,7 +345,7 @@ public class VsmCommand { s_logger.error("Error while creating hello message : " + e.getMessage()); return null; } catch (DOMException e) { - s_logger.error("Error while creating delete message : " + e.getMessage()); + s_logger.error("Error while creating hello message : " + e.getMessage()); return null; } } diff --git a/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmOkResponse.java b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmOkResponse.java index bdcffce7ef4..ed25238ed1f 100644 --- a/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmOkResponse.java +++ b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmOkResponse.java @@ -7,6 +7,7 @@ public class VsmOkResponse extends VsmResponse { VsmOkResponse(String response) { super(response); + initialize(); } protected void parse(Element root) { diff --git a/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmPolicyMapResponse.java b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmPolicyMapResponse.java new file mode 100644 index 00000000000..ffd2088a9d6 --- /dev/null +++ b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmPolicyMapResponse.java @@ -0,0 +1,66 @@ +package com.cloud.utils.cisco.n1kv.vsm; + +import org.apache.log4j.Logger; + +import org.w3c.dom.DOMException; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public class VsmPolicyMapResponse extends VsmResponse{ + private static final Logger s_logger = Logger.getLogger(VsmPolicyMapResponse.class); + private static final String s_policyMapDetails = "__XML__OPT_Cmd_show_policy-map___readonly__"; + + private PolicyMap _policyMap = new PolicyMap(); + + VsmPolicyMapResponse(String response) { + super(response); + initialize(); + } + + public PolicyMap getPolicyMap() { + return _policyMap; + } + + protected void parse(Element root) { + NodeList list = root.getElementsByTagName("nf:rpc-error"); + if (list.getLength() == 0) { + // No rpc-error tag; means response was ok. + NodeList dataList = root.getElementsByTagName("nf:data"); + if (dataList.getLength() > 0) { + parseData(dataList.item(0)); + _responseOk = true; + } + } else { + super.parseError(list.item(0)); + _responseOk = false; + } + } + + protected void parseData(Node data) { + try { + NodeList list = ((Element)data).getElementsByTagName(s_policyMapDetails); + if (list.getLength() > 0) { + NodeList readOnlyList = ((Element)list.item(0)).getElementsByTagName("__readonly__"); + Element readOnly = (Element)readOnlyList.item(0); + + for (Node node = readOnly.getFirstChild(); + node != null; node = node.getNextSibling()) { + String currentNode = node.getNodeName(); + String value = node.getTextContent(); + if ("pmap-name-out".equalsIgnoreCase(currentNode)) { + _policyMap.policyMapName = value; + } else if ("cir".equalsIgnoreCase(currentNode)) { + _policyMap.committedRate = Integer.parseInt(value.trim()); + } else if ("bc".equalsIgnoreCase(currentNode)) { + _policyMap.burstRate = Integer.parseInt(value.trim()); + } else if ("pir".equalsIgnoreCase(currentNode)) { + _policyMap.peakRate = Integer.parseInt(value.trim()); + } + } + } + } catch (DOMException e) { + s_logger.error("Error parsing the response : " + e.toString()); + } + } +} diff --git a/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmPortProfileResponse.java b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmPortProfileResponse.java index ed43bd8f1cc..6973c0ec828 100644 --- a/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmPortProfileResponse.java +++ b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmPortProfileResponse.java @@ -1,22 +1,145 @@ package com.cloud.utils.cisco.n1kv.vsm; +import org.apache.log4j.Logger; + +import org.w3c.dom.DOMException; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import java.util.StringTokenizer; + +import com.cloud.utils.cisco.n1kv.vsm.VsmCommand.*; public class VsmPortProfileResponse extends VsmResponse { + private static final Logger s_logger = Logger.getLogger(VsmPortProfileResponse.class); + private static final String s_portProfileDetails = "__XML__OPT_Cmd_show_port_profile___readonly__"; + + private PortProfile _portProfile = new PortProfile(); + VsmPortProfileResponse(String response) { super(response); + initialize(); + } + + public PortProfile getPortProfile() { + return _portProfile; } protected void parse(Element root) { NodeList list = root.getElementsByTagName("nf:rpc-error"); if (list.getLength() == 0) { // No rpc-error tag; means response was ok. - assert(root.getElementsByTagName("nf:ok").getLength() > 0); - _responseOk = true; + NodeList dataList = root.getElementsByTagName("nf:data"); + if (dataList.getLength() > 0) { + parseData(dataList.item(0)); + _responseOk = true; + } } else { super.parseError(list.item(0)); _responseOk = false; } } + + protected void parseData(Node data) { + try { + NodeList list = ((Element)data).getElementsByTagName(s_portProfileDetails); + if (list.getLength() > 0) { + NodeList readOnlyList = ((Element)list.item(0)).getElementsByTagName("__readonly__"); + Element readOnly = (Element)readOnlyList.item(0); + + for (Node node = readOnly.getFirstChild(); + node != null; node = node.getNextSibling()) { + String currentNode = node.getNodeName(); + String value = node.getTextContent(); + if ("port_binding".equalsIgnoreCase(currentNode)) { + setPortBinding(value); + } else if ("profile_name".equalsIgnoreCase(currentNode)) { + // Set the port profile name. + _portProfile.profileName = value; + } else if ("profile_cfg".equalsIgnoreCase(currentNode)) { + setProfileConfiguration(value); + } else if ("type".equalsIgnoreCase(currentNode)) { + setPortType(value); + } else if ("status".equalsIgnoreCase(currentNode)) { + // Has the profile been enabled. + if (value.equalsIgnoreCase("1")) { + _portProfile.status = true; + } + } else if ("max_ports".equalsIgnoreCase(currentNode)) { + // Has the profile been enabled. + _portProfile.maxPorts = Integer.parseInt(value.trim()); + } + } + } + } catch (DOMException e) { + s_logger.error("Error parsing the response : " + e.toString()); + } + } + + private void setProfileConfiguration(String value) { + StringTokenizer tokens = new StringTokenizer(value.trim()); + if (tokens.hasMoreTokens()) { + String currentToken = tokens.nextToken(); + if ("switchport".equalsIgnoreCase(currentToken)) { + parseProfileMode(tokens); + } else if ("service-policy".equalsIgnoreCase(currentToken)) { + String ioType = tokens.nextToken(); + if ("input".equalsIgnoreCase(ioType)) { + _portProfile.inputPolicyMap = tokens.nextToken(); + } else if ("output".equalsIgnoreCase(ioType)) { + _portProfile.outputPolicyMap = tokens.nextToken(); + } + } + } + } + + private void parseProfileMode(StringTokenizer tokens) { + if (tokens.hasMoreTokens()) { + String firstToken = tokens.nextToken(); + if ("mode".equalsIgnoreCase(firstToken)) { + setPortMode(tokens.nextToken()); + } else if ("access".equalsIgnoreCase(firstToken)) { + if (tokens.hasMoreTokens()) { + String secondToken = tokens.nextToken(); + assert("vlan".equalsIgnoreCase(secondToken)); + if (tokens.hasMoreTokens()) { + _portProfile.vlan = tokens.nextToken(); + } + } + } + } + } + + private void setPortMode(String value) { + // Set the mode for port profile. + if ("access".equalsIgnoreCase(value)) { + _portProfile.mode = SwitchPortMode.access; + } else if ("trunk".equalsIgnoreCase(value)) { + _portProfile.mode = SwitchPortMode.trunk; + } else if ("privatevlanhost".equalsIgnoreCase(value)) { + _portProfile.mode = SwitchPortMode.privatevlanhost; + } else if ("privatevlanpromiscuous".equalsIgnoreCase(value)) { + _portProfile.mode = SwitchPortMode.privatevlanpromiscuous; + } + } + + private void setPortBinding(String value) { + // Set the binding type for the port profile. + if ("static".equalsIgnoreCase(value)) { + _portProfile.binding = BindingType.portbindingstatic; + } else if ("dynamic".equalsIgnoreCase(value)) { + _portProfile.binding = BindingType.portbindingdynamic; + } else if ("ephermal".equalsIgnoreCase(value)) { + _portProfile.binding = BindingType.portbindingephermal; + } + } + + private void setPortType(String value) { + // Set the type field (vethernet/ethernet). + if ("vethernet".equalsIgnoreCase(value)) { + _portProfile.type = PortProfileType.vethernet; + } else if ("ethernet".equalsIgnoreCase(value)) { + _portProfile.type = PortProfileType.ethernet; + } + } } diff --git a/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmResponse.java b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmResponse.java index 6391f12d210..6270d45f912 100644 --- a/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmResponse.java +++ b/utils/src/com/cloud/utils/cisco/n1kv/vsm/VsmResponse.java @@ -8,7 +8,6 @@ import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; -import org.w3c.dom.NodeList; import org.w3c.dom.DOMException; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSSerializer; @@ -73,7 +72,10 @@ public abstract class VsmResponse { _tag = ErrorTag.InUse; _type = ErrorType.rpc; _severity = ErrorSeverity.error; + _docResponse = null; + } + protected void initialize() { try { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); docFactory.setNamespaceAware(true); From f635b96b89c994202a4f01963f46196d394b7680 Mon Sep 17 00:00:00 2001 From: Brian Federle Date: Tue, 22 May 2012 11:19:19 -0700 Subject: [PATCH 06/14] CS-15045 commit 7859d289debae622a15fdb6bda854a8936fd35cb Author: Pranav Saxena Date: Tue May 22 23:29:31 2012 +0530 UI does not support RAW format when hypervisor used is OVM in upload volumes --- ui/scripts/storage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/scripts/storage.js b/ui/scripts/storage.js index 2f70ef8cb32..21cbeed5ef2 100644 --- a/ui/scripts/storage.js +++ b/ui/scripts/storage.js @@ -207,6 +207,7 @@ label: 'label.format', select: function(args) { var items = []; + items.push({ id: 'RAW', description: 'RAW' }); items.push({ id: 'VHD', description: 'VHD' }); items.push({ id: 'OVA', description: 'OVA' }); items.push({ id: 'QCOW2', description: 'QCOW2' }); From 2ae25b7a2ff287a0ace29ad0103320444615ec48 Mon Sep 17 00:00:00 2001 From: Brian Federle Date: Tue, 22 May 2012 11:45:58 -0700 Subject: [PATCH 07/14] CS-14953: 'CloudStack' -> 'CloudPlatform' reviewed-by: brian commit 4488f0a66766286e960a47d34cd2e5148162bcab Author: Pranav Saxena Date: Tue May 22 15:15:40 2012 +0530 Renaming CloudStack to CloudPlatform for Proprietary builds --- ui/css/cloudstack3.css | 12 ++++--- ui/images/bg-what-is-cloudplatform.png | Bin 0 -> 32250 bytes ui/images/bg-what-is-cloudstack.png | Bin 32434 -> 32240 bytes ui/scripts/ui-custom/installWizard.js | 48 +++++++++++++++++-------- 4 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 ui/images/bg-what-is-cloudplatform.png diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 36ca9af4264..3c20946935e 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -557,6 +557,11 @@ body.login { height: 540px; } +.install-wizard .step.intro.what-is-cloudplatform p { + background: url(../images/bg-what-is-cloudplatform.png) no-repeat 50% 237px; + height: 540px; +} + /*** Diagram*/ .install-wizard .diagram { width: 910px; @@ -7989,7 +7994,7 @@ div.panel.ui-dialog div.list-view div.fixed-header { .project-selector .button.cancel { color: #808080; - background: url("../images/gradients.png") repeat scroll 0 -480px #B6B6B6; + background: #B6B6B6 url("../images/gradients.png") repeat 0 -480px; border: 1px solid #AAAAAA; border-radius: 4px 4px 4px 4px; font-size: 13px; @@ -7999,19 +8004,18 @@ div.panel.ui-dialog div.list-view div.fixed-header { color: #838181; /*+placement:shift 488px 9px;*/ position: relative; + left: 488px; + top: 9px; left: 170px; top: -8px; margin: 19px 0 0 0px; width: 54px; - } .project-selector .button.cancel:hover { color: #3A3A3A; - } - /*** Resource management*/ .project-dashboard .resources { } diff --git a/ui/images/bg-what-is-cloudplatform.png b/ui/images/bg-what-is-cloudplatform.png new file mode 100644 index 0000000000000000000000000000000000000000..138c63bbaea5efabc8b92b8781b78b6b39c90ae0 GIT binary patch literal 32250 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 z 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 delta 31365 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

_KpgAV{$>rV=mBB&16%6@qG9* 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 diff --git a/ui/scripts/ui-custom/installWizard.js b/ui/scripts/ui-custom/installWizard.js index 4dcad5a7d6a..ee2fcd86a73 100644 --- a/ui/scripts/ui-custom/installWizard.js +++ b/ui/scripts/ui-custom/installWizard.js @@ -20,6 +20,18 @@ var launchStart; // Holds last launch callback, in case of error var $launchState; + // Checking if title should be ‘CloudStack’ or ‘CloudPlatform’ – replacing occurrences thereby + var checkTitle = function(str) { + // Getting the flag that indicates if EULA is present + if (eulaHTML && eulaHTML.length) { return str.replace(/CloudStack/ig,'CloudPlatform'); } + else { return str; } + } + + /* var data = $("p"); + $(data).each(function() { + $(this).html(checkTitle($(this).html())); + }); +*/ /** * Successful installation action */ @@ -39,7 +51,7 @@ cloudStack.installWizard.copy[id]({ response: { success: function(args) { - $elem.append(_l(args.text)); + $elem.append(checkTitle(_l(args.text))); } } }); @@ -87,9 +99,9 @@ var $intro = $('
').addClass('intro'); var $title = $('
').addClass('title') - .html(title); + .html(checkTitle(title)); var $subtitle = $('
').addClass('subtitle') - .html(subtitle); + .html(checkTitle(subtitle)); var $copy = getCopy(copyID, $('

')); var $prev = elems.prevButton(_l('label.back')); var $continue = elems.nextButton('OK'); @@ -202,8 +214,8 @@ tooltip: function(title, content) { return $('
').addClass('tooltip-info').append( $('
').addClass('arrow'), - $('
').addClass('title').html(_l(title)), - $('
').addClass('content').append($('

').html(_l(content))) + $('

').addClass('title').html(checkTitle(_l(title))), + $('
').addClass('content').append($('

').html(checkTitle(_l(content)))) ); }, @@ -214,8 +226,8 @@ return $('

').addClass('header') .append( $.merge( - $('

').html(_l('label.installWizard.title')), - $('

').html(_l('label.installWizard.subtitle')) + $('

').html(checkTitle(_l('label.installWizard.title'))), + $('

').html(checkTitle(_l('label.installWizard.subtitle'))) ) ); }, @@ -297,8 +309,8 @@ var steps = { eula: function(args) { var $intro = $('
').addClass('intro eula'); - var $title = $('
').addClass('title').html(_l('label.license.agreement')); - var $subtitle = $('
').addClass('subtitle').html(_l('label.license.agreement.subtitle')); + var $title = $('
').addClass('title').html(checkTitle(_l('label.license.agreement'))); + var $subtitle = $('
').addClass('subtitle').html(checkTitle(_l('label.license.agreement.subtitle'))); var $copy = $('
').addClass('eula-copy').html(eulaHTML); var $continue = elems.nextButton(_l('label.agree')); @@ -314,12 +326,17 @@ }, intro: function(args) { - var $intro = $('
').addClass('intro what-is-cloudstack'); - var $title = $('
').addClass('title').html(_l('label.what.is.cloudstack')); - var $subtitle = $('
').addClass('subtitle').html(_l('label.introduction.to.cloudstack')); + if (eulaHTML && eulaHTML.length){ + var $intro = $('
').addClass('intro what-is-cloudplatform'); } + else { + var $intro = $('
').addClass('intro what-is-cloudstack'); + + } + var $title = $('
').addClass('title').html(checkTitle(_l('label.what.is.cloudstack'))); + var $subtitle = $('
').addClass('subtitle').html(checkTitle(_l('label.introduction.to.cloudstack'))); var $copy = getCopy('whatIsCloudStack', $('

')); var $continue = elems.nextButton(_l('label.continue.basic.install')); - var $advanced = elems.nextButton(_l('label.skip.guide')).addClass('advanced-installation'); + var $advanced = elems.nextButton(checkTitle(_l('label.skip.guide'))).addClass('advanced-installation'); $continue.click(function() { goTo('changeUser'); @@ -792,6 +809,7 @@ var initialStep = eulaHTML ? steps.eula().addClass('step') : steps.intro().addClass('step'); + showDiagram(''); $('html body').addClass('install-wizard'); @@ -801,7 +819,9 @@ elems.body().append(initialStep), $diagramParts ).appendTo($container); - }; + + + }; cloudStack.uiCustom.installWizard = installWizard; }(jQuery, cloudStack)); From fb362127266d063b11ad299872222d990aca5f22 Mon Sep 17 00:00:00 2001 From: Brian Federle Date: Tue, 22 May 2012 11:59:05 -0700 Subject: [PATCH 08/14] CS-14911: Fix missing 'mine' filter from instances reviewed-by: brian commit 98ead3ad5b9266e0f2a1b0e037a4f511d9e1e240 Author: Pranav Saxena Date: Wed May 16 01:04:12 2012 +0530 CS-14911 : mine is missing from Instance Filter --- ui/scripts/instances.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 3af5761eaaa..dee0cf0f307 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -23,6 +23,7 @@ section: 'instances', filters: { all: { label: 'ui.listView.filters.all' }, + mine: { label: 'ui.listView.filters.mine' }, running: { label: 'state.Running' }, stopped: { label: 'state.Stopped' }, destroyed: { From 6401205d5d1fa8a5e6afdcb2ffd11500f430e86e Mon Sep 17 00:00:00 2001 From: Brian Federle Date: Tue, 22 May 2012 12:11:47 -0700 Subject: [PATCH 09/14] CS-13796: Fix typos in onscreen text reviewed-by: brian commit c16320209eb5b4cde019c6dbe1480160fa8097df Author: Pranav Saxena Date: Tue Apr 24 02:06:43 2012 +0530 CS-13796: Typos in Onscreen text --- client/WEB-INF/classes/resources/messages.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/WEB-INF/classes/resources/messages.properties b/client/WEB-INF/classes/resources/messages.properties index 4c566c7ed13..f51a5f2a6b7 100644 --- a/client/WEB-INF/classes/resources/messages.properties +++ b/client/WEB-INF/classes/resources/messages.properties @@ -114,7 +114,7 @@ label.corrections.saved=Corrections saved message.installWizard.copy.whatIsSecondaryStorage=Secondary storage is associated with a zone, and it stores the following:
  • Templates - OS images that can be used to boot VMs and can include additional configuration information, such as installed applications
  • ISO images - OS images that can be bootable or non-bootable
  • Disk volume snapshots - saved copies of VM data which can be used for data recovery or to create new templates
message.installWizard.copy.whatIsPrimaryStorage=A CloudStack™ cloud infrastructure makes use of two types of storage: primary storage and secondary storage. Both of these can be iSCSI or NFS servers, or localdisk.

Primary storage is associated with a cluster, and it stores the disk volumes of each guest VM for all the VMs running on hosts in that cluster. The primary storage server is typically located close to the hosts. message.installWizard.copy.whatIsACluster=A cluster provides a way to group hosts. The hosts in a cluster all have identical hardware, run the same hypervisor, are on the same subnet, and access the same shared storage. Virtual machine instances (VMs) can be live-migrated from one host to another within the same cluster, without interrupting service to the user. A cluster is the third-largest organizational unit within a CloudStack™ deployment. Clusters are contained within pods, and pods are contained within zones.

CloudStack™ allows multiple clusters in a cloud deployment, but for a Basic Installation, we only need one cluster. -message.installWizard.copy.whatIsAPod=A pod often represents a single rack. Hosts in the same pod are in the same subnet.

A pod is the second-largest organizational unit within a CloudStack™ deployment. Pods are contained within zones. Each zone can contain one or more pods; in the Basic Installation, you will have just one pod in your zone +message.installWizard.copy.whatIsAPod=A pod often represents a single rack. Hosts in the same pod are in the same subnet.

A pod is the second-largest organizational unit within a CloudStack™ deployment. Pods are contained within zones. Each zone can contain one or more pods; in the Basic Installation, you will have just one pod in your zone. message.installWizard.copy.whatIsAZone=A zone is the largest organizational unit within a CloudStack™ deployment. A zone typically corresponds to a single datacenter, although it is permissible to have multiple zones in a datacenter. The benefit of organizing infrastructure into zones is to provide physical isolation and redundancy. For example, each zone can have its own power supply and network uplink, and the zones can be widely separated geographically (though this is not required). message.installWizard.copy.whatIsCloudStack=CloudStack™ is a software platform that pools computing resources to build public, private, and hybrid Infrastructure as a Service (IaaS) clouds. CloudStack™ manages the network, storage, and compute nodes that make up a cloud infrastructure. Use CloudStack™ to deploy, manage, and configure cloud computing environments.

Extending beyond individual virtual machine images running on commodity hardware, CloudStack™ provides a turnkey cloud infrastructure software stack for delivering virtual datacenters as a service - delivering all of the essential components to build, deploy, and manage multi-tier and multi-tenant cloud applications. Both open-source and Premium versions are available, with the open-source version offering nearly identical features. message.installWizard.tooltip.addSecondaryStorage.path=The exported path, located on the server you specified above @@ -504,7 +504,7 @@ label.add.resources=Add Resources label.launch=Launch label.set.up.zone.type=Set up zone type message.please.select.a.configuration.for.your.zone=Please select a configuration for your zone. -message.desc.basic.zone=Provide a single network where each VM instance is assigned an IP directly from the network. Guest isolation can be provided through layer-3 means such as security groups (IP address source filtering) +message.desc.basic.zone=Provide a single network where each VM instance is assigned an IP directly from the network. Guest isolation can be provided through layer-3 means such as security groups (IP address source filtering). label.basic=Basic message.desc.advanced.zone=For more sophisticated network topologies. This network model provides the most flexibility in defining guest networks and providing custom network offerings such as firewall, VPN, or load balancer support. label.advanced=Advanced From f854455df7476387abfb549208783e7f3aa85a99 Mon Sep 17 00:00:00 2001 From: Brian Federle Date: Tue, 22 May 2012 12:32:43 -0700 Subject: [PATCH 10/14] Use IE-compatible method for changing browser title --- ui/scripts/cloudStack.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js index 336a2aa09b7..1ea0b169212 100644 --- a/ui/scripts/cloudStack.js +++ b/ui/scripts/cloudStack.js @@ -382,11 +382,11 @@ url: 'eula.' + g_lang + '.html', dataType: 'html', success: function(html) { - $('title').html('CloudPlatform'); + document.title = 'CloudPlatform'; cloudStack.uiCustom.login($.extend(loginArgs, { eula: html, hasLogo: true })); }, error: function() { - $('title').html('CloudStack'); + document.title = 'CloudStack'; cloudStack.uiCustom.login(loginArgs); }, beforeSend : function(XMLHttpResponse) { From d0b6bc7d2bf8abadaa5393be1a1cf6c9a8a13664 Mon Sep 17 00:00:00 2001 From: Brian Federle Date: Tue, 22 May 2012 12:33:42 -0700 Subject: [PATCH 11/14] Remove trailing comma --- ui/scripts/storage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/scripts/storage.js b/ui/scripts/storage.js index 21cbeed5ef2..6f90d699f21 100644 --- a/ui/scripts/storage.js +++ b/ui/scripts/storage.js @@ -34,7 +34,7 @@ name: { label: 'label.name' }, type: { label: 'label.type' }, hypervisor: { label: 'label.hypervisor' }, - vmdisplayname: { label: 'label.vm.display.name' }, + vmdisplayname: { label: 'label.vm.display.name' } /* state: { From 58c8254dd2a1b12ab75b0300b1d13c32540401b6 Mon Sep 17 00:00:00 2001 From: Brian Federle Date: Tue, 22 May 2012 13:45:35 -0700 Subject: [PATCH 12/14] Add cluster: conditionally hide vSwitch fields -When in add cluster screen, show the add vSwitch fields when hypervisor == VMware and 'vmware.use.nexus.vswitch' configuration flag is enabled. -Remove 'add Nexus vSwitch' checkbox, as the vSwitch fields will always be shown for VMware if the above config flag is set. --- ui/scripts/system.js | 49 +++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/ui/scripts/system.js b/ui/scripts/system.js index f5f1d53c6cb..bcefbdb4748 100644 --- a/ui/scripts/system.js +++ b/ui/scripts/system.js @@ -6079,6 +6079,8 @@ hypervisor: { label: 'label.hypervisor', select: function(args) { + var vSwitchEnabled = false; + $.ajax({ url: createURL("listHypervisors"), dataType: "json", @@ -6093,31 +6095,47 @@ } }); + // Check whether vSwitch capability is enabled + $.ajax({ + url: createURL('listConfigurations'), + data: { + name: 'vmware.use.nexus.vswitch' + }, + async: false, + success: function(json) { + if (json.listconfigurationsresponse.configuration[0].value == 'true') { + vSwitchEnabled = true; + } + } + }); + args.$select.bind("change", function(event) { var $form = $(this).closest('form'); - if($(this).val() == "VMware") { + var $vsmFields = $.merge( + $form.find('.form-item[rel=vsmipaddress]'), + $form.find('.form-item[rel=vsmusername]'), + $form.find('.form-item[rel=vsmpassword]') + ); + + if ($(this).val() == "VMware") { //$('li[input_sub_group="external"]', $dialogAddCluster).show(); $form.find('.form-item[rel=vCenterHost]').css('display', 'inline-block'); $form.find('.form-item[rel=vCenterUsername]').css('display', 'inline-block'); $form.find('.form-item[rel=vCenterPassword]').css('display', 'inline-block'); $form.find('.form-item[rel=vCenterDatacenter]').css('display', 'inline-block'); - $form.find('.form-item[rel=enableNexusVswitch]').css('display', 'inline-block'); - //$("#cluster_name_label", $dialogAddCluster).text("vCenter Cluster:"); - } - else { - //$('li[input_group="vmware"]', $dialogAddCluster).hide(); + if (vSwitchEnabled) { + $vsmFields.css('display', 'inline-block'); + } else { + $vsmFields.css('display', 'none'); + } + } else { $form.find('.form-item[rel=vCenterHost]').css('display', 'none'); $form.find('.form-item[rel=vCenterUsername]').css('display', 'none'); $form.find('.form-item[rel=vCenterPassword]').css('display', 'none'); $form.find('.form-item[rel=vCenterDatacenter]').css('display', 'none'); $form.find('.form-item[rel=enableNexusVswitch]').css('display', 'none'); - $('.form-item[rel=enableNexusVswitch] input').attr('checked', false); - $form.find('.form-item[rel=nexusVswitchIpAddress]').css('display', 'none'); - $form.find('.form-item[rel=nexusVswitchUsername]').css('display', 'none'); - $form.find('.form-item[rel=nexusVswitchPassword]').css('display', 'none'); - - //$("#cluster_name_label", $dialogAddCluster).text("Cluster:"); + $vsmFields.css('display', 'none'); } }); } @@ -6167,25 +6185,18 @@ label: 'label.vcenter.datacenter', validation: { required: true } }, - enableNexusVswitch: { - label: 'Add Nexus vSwitch', - isBoolean: true - }, vsmipaddress: { label: 'vSwitch IP Address', - dependsOn: 'enableNexusVswitch', validation: { required: true }, isHidden: true }, vsmusername: { label: 'vSwitch Username', - dependsOn: 'enableNexusVswitch', validation: { required: true }, isHidden: true }, vsmpassword: { label: 'vSwitch Password', - dependsOn: 'enableNexusVswitch', validation: { required: true }, isPassword: true, isHidden: true From fddafbce0b73943d119a05304e99d88c5ff6027a Mon Sep 17 00:00:00 2001 From: Jessica Wang Date: Tue, 22 May 2012 14:48:03 -0700 Subject: [PATCH 13/14] CS-14206: cloudstack 3.0 UI - Create Network Offering dialog - When Guest Type is "Shared", include LB and StaticNAT in services regardless of zone type. --- ui/scripts/configuration.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ui/scripts/configuration.js b/ui/scripts/configuration.js index 932a172913c..1c156df5d66 100644 --- a/ui/scripts/configuration.js +++ b/ui/scripts/configuration.js @@ -1079,6 +1079,15 @@ //hide/show service fields upon guestIpType(Shared/Isolated) and zoneType(Advanced/Basic) ***** (begin) ***** var serviceFieldsToHide = []; if($guestTypeField.val() == 'Shared') { //Shared network offering + serviceFieldsToHide = [ + 'service.SourceNat.isEnabled', + 'service.PortForwarding.isEnabled', + 'service.Firewall.isEnabled', + 'service.Vpn.isEnabled' + ]; + + //CS-14206 + /* if (hasAdvancedZones) { //advanced zone serviceFieldsToHide = [ 'service.SourceNat.isEnabled', @@ -1097,6 +1106,8 @@ 'service.Vpn.isEnabled' ]; } + */ + } else { //Isolated network offering (which supports all services) serviceFieldsToHide = []; From ce07dcff8560cb4eff1f52483b6fb88d42b507c6 Mon Sep 17 00:00:00 2001 From: Jessica Wang Date: Tue, 22 May 2012 15:35:28 -0700 Subject: [PATCH 14/14] CS-15042: cloudstack 3.0 UI - Create Network Offering dialog - when Guest Type is "Isolated", exclude "Security Groups" from services list. --- ui/scripts/configuration.js | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/ui/scripts/configuration.js b/ui/scripts/configuration.js index 1c156df5d66..92c0d49b8e2 100644 --- a/ui/scripts/configuration.js +++ b/ui/scripts/configuration.js @@ -1084,33 +1084,12 @@ 'service.PortForwarding.isEnabled', 'service.Firewall.isEnabled', 'service.Vpn.isEnabled' - ]; - - //CS-14206 - /* - if (hasAdvancedZones) { //advanced zone - serviceFieldsToHide = [ - 'service.SourceNat.isEnabled', - 'service.StaticNat.isEnabled', - 'service.PortForwarding.isEnabled', - 'service.Lb.isEnabled', - 'service.Firewall.isEnabled', - 'service.Vpn.isEnabled' - ]; - } - else { //basic zone - serviceFieldsToHide = [ - 'service.SourceNat.isEnabled', - 'service.PortForwarding.isEnabled', - 'service.Firewall.isEnabled', - 'service.Vpn.isEnabled' - ]; - } - */ - + ]; } - else { //Isolated network offering (which supports all services) - serviceFieldsToHide = []; + else { //Isolated network offering + serviceFieldsToHide = [ + 'service.SecurityGroup.isEnabled' + ]; } //hide service fields that are included in serviceFieldsToHide