|
43 | 43 | from marvin.cloudstackTestCase import cloudstackTestCase |
44 | 44 | from marvin.cloudstackAPI import (listPhysicalNetworks, |
45 | 45 | listTrafficTypes, |
| 46 | + listManagementServers, |
46 | 47 | listNetworkServiceProviders, |
47 | 48 | updateNetworkServiceProvider, |
48 | 49 | deleteNetworkServiceProvider, |
@@ -393,8 +394,35 @@ def setUpClass(cls): |
393 | 394 | cls.logger.setLevel(logging.DEBUG) |
394 | 395 | cls.logger.addHandler(cls.stream_handler) |
395 | 396 |
|
396 | | - cls.logger.info("Management servers: %s", |
397 | | - [m.get("mgtSvrIp", "?") for m in cls.all_mgt_svr_details]) |
| 397 | + cls.logger.info("Management servers (from config): %s", |
| 398 | + [m.get("mgtSvrIp", "?") for m in cls.all_mgt_svr_details]) |
| 399 | + |
| 400 | + # Supplement / override management server list via listManagementServers API. |
| 401 | + # All mgmt servers are assumed to share the same SSH credentials as the |
| 402 | + # first entry in the Marvin config (mgtSvr[0]). |
| 403 | + try: |
| 404 | + ms_cmd = listManagementServers.listManagementServersCmd() |
| 405 | + api_mgmt_servers = cls.apiclient.listManagementServers(ms_cmd) |
| 406 | + if api_mgmt_servers: |
| 407 | + base_creds = dict(cls.mgtSvrDetails) |
| 408 | + api_mgt_details = [] |
| 409 | + for ms in api_mgmt_servers: |
| 410 | + ip = (getattr(ms, 'ipaddress', None) |
| 411 | + or getattr(ms, 'ip', None) |
| 412 | + or getattr(ms, 'hostname', None)) |
| 413 | + if ip: |
| 414 | + entry = dict(base_creds) |
| 415 | + entry['mgtSvrIp'] = ip |
| 416 | + api_mgt_details.append(entry) |
| 417 | + if api_mgt_details: |
| 418 | + cls.all_mgt_svr_details = api_mgt_details |
| 419 | + cls.logger.info( |
| 420 | + "Management servers (from listManagementServers API): %s", |
| 421 | + [d.get('mgtSvrIp') for d in api_mgt_details]) |
| 422 | + except Exception as _ms_err: |
| 423 | + cls.logger.warning( |
| 424 | + "Could not retrieve management servers via listManagementServers " |
| 425 | + "API (%s); using config-provided list", _ms_err) |
398 | 426 |
|
399 | 427 | # KVM host credentials from Marvin config |
400 | 428 | cls.kvm_host_configs = _get_kvm_hosts_from_config(cls.config) |
@@ -701,6 +729,71 @@ def _get_source_nat_ip(self, network_id): |
701 | 729 | self.logger.warning("_get_source_nat_ip(%s): %s", network_id, e) |
702 | 730 | return None |
703 | 731 |
|
| 732 | + # ------------------------------------------------------------------ |
| 733 | + # KVM host prerequisite check (test_04 / test_05 / test_06) |
| 734 | + # ------------------------------------------------------------------ |
| 735 | + |
| 736 | + def _check_kvm_host_prerequisites(self, tools=None): |
| 737 | + """Verify that each configured KVM host has the required tools installed. |
| 738 | +
|
| 739 | + Checks for the presence of every tool in *tools* (default: |
| 740 | + ``['arping', 'dnsmasq', 'haproxy']``) on each host in |
| 741 | + ``self.kvm_host_configs`` via SSH. The test is skipped (via |
| 742 | + ``skipTest``) if any tool is absent from any reachable host. |
| 743 | +
|
| 744 | + Hosts that cannot be reached over SSH are logged as warnings and |
| 745 | + excluded from the check — the connectivity failure will surface |
| 746 | + naturally when the test later tries to deploy scripts. |
| 747 | + """ |
| 748 | + if tools is None: |
| 749 | + tools = ['arping', 'dnsmasq', 'haproxy'] |
| 750 | + if not self.kvm_host_configs: |
| 751 | + # No KVM hosts — _setup_extension_nsp_offering will skipTest. |
| 752 | + return |
| 753 | + |
| 754 | + missing_per_host = {} |
| 755 | + for h in self.kvm_host_configs: |
| 756 | + ip = h.get('ip', '') |
| 757 | + if not ip: |
| 758 | + continue |
| 759 | + username = h.get('username', 'root') |
| 760 | + password = h.get('password', '') |
| 761 | + try: |
| 762 | + ssh = SshClient(ip, 22, username, password) |
| 763 | + missing_tools = [] |
| 764 | + for tool in tools: |
| 765 | + # Try both `command -v` (bash built-in) and `which` |
| 766 | + out = ssh.execute( |
| 767 | + "command -v {t} 2>/dev/null || which {t} 2>/dev/null" |
| 768 | + " || echo MISSING_{t}".format(t=tool)) |
| 769 | + found = any( |
| 770 | + line.strip() and 'MISSING_' + tool not in line |
| 771 | + for line in out |
| 772 | + ) |
| 773 | + if not found: |
| 774 | + missing_tools.append(tool) |
| 775 | + if missing_tools: |
| 776 | + missing_per_host[ip] = missing_tools |
| 777 | + self.logger.warning( |
| 778 | + "KVM host %s is missing prerequisite(s): %s", |
| 779 | + ip, ', '.join(missing_tools)) |
| 780 | + else: |
| 781 | + self.logger.info( |
| 782 | + "KVM host %s: all prerequisites present (%s)", |
| 783 | + ip, ', '.join(tools)) |
| 784 | + except Exception as e: |
| 785 | + self.logger.warning( |
| 786 | + "Could not check prerequisites on KVM host %s: %s", ip, e) |
| 787 | + |
| 788 | + if missing_per_host: |
| 789 | + detail = "; ".join( |
| 790 | + "%s missing %s" % (ip, ', '.join(t)) |
| 791 | + for ip, t in missing_per_host.items() |
| 792 | + ) |
| 793 | + self.skipTest( |
| 794 | + "Skipping test — required tools not installed on KVM host(s): " |
| 795 | + + detail) |
| 796 | + |
704 | 797 | # ------------------------------------------------------------------ |
705 | 798 | # Extension + NSP + offering setup helper (shared by tests 04-06) |
706 | 799 | # ------------------------------------------------------------------ |
@@ -1062,6 +1155,7 @@ def test_04_dhcp_dns_userdata(self): |
1062 | 1155 | 3. Assert VM state == Running. |
1063 | 1156 | 4. Teardown. |
1064 | 1157 | """ |
| 1158 | + self._check_kvm_host_prerequisites(['arping', 'dnsmasq', 'haproxy']) |
1065 | 1159 | svc = "Dhcp,Dns,UserData" |
1066 | 1160 | nw_offering, _ext_name = self._setup_extension_nsp_offering( |
1067 | 1161 | "extnet-dhcp", supported_services=svc, guestiptype="Shared") |
@@ -1117,6 +1211,7 @@ def test_05_isolated_network_full_lifecycle(self): |
1117 | 1211 | → restartNetwork(cleanup=True) |
1118 | 1212 | → assert SSH works (namespace rebuilt, rules reapplied) |
1119 | 1213 | """ |
| 1214 | + self._check_kvm_host_prerequisites(['arping', 'dnsmasq', 'haproxy']) |
1120 | 1215 | # ---- Setup ---- |
1121 | 1216 | svc = "SourceNat,StaticNat,PortForwarding,Firewall,Lb,UserData,Dhcp,Dns" |
1122 | 1217 | nw_offering, _ext_name = self._setup_extension_nsp_offering( |
@@ -1329,6 +1424,7 @@ def test_06_vpc_multi_tier_and_restart(self): |
1329 | 1424 | The VPC tier network offering uses ``useVpc=on`` as required by |
1330 | 1425 | CloudStack for VPC-associated tier networks. |
1331 | 1426 | """ |
| 1427 | + self._check_kvm_host_prerequisites(['arping', 'dnsmasq', 'haproxy']) |
1332 | 1428 | # ---- Setup: extension + NSP only (no isolated offering for VPC tests) ---- |
1333 | 1429 | svc = "SourceNat,StaticNat,PortForwarding,Lb,UserData,Dhcp,Dns" |
1334 | 1430 | _nw_offering, ext_name = self._setup_extension_nsp_offering( |
|
0 commit comments