diff --git a/.gitignore b/.gitignore index 2542551..5c4210d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /virt-who-0.24.1.tar.gz /virt-who-0.24.2.tar.gz /virt-who-0.29.0.tar.gz +/virt-who-0.31.0.tar.gz +/build-rpm-no-ahv.patch diff --git a/build-rpm-no-ahv.patch b/build-rpm-no-ahv.patch new file mode 100644 index 0000000..33efb5c --- /dev/null +++ b/build-rpm-no-ahv.patch @@ -0,0 +1,1829 @@ +From 0f2217ceffcf4f8d87cf5a8634542901be8b65fe Mon Sep 17 00:00:00 2001 +From: Kevin Howell +Date: Fri, 18 Oct 2019 09:54:54 -0400 +Subject: [PATCH] Remove AHV support + +--- + tests/test_ahv.py | 566 --------------------- + virtwho/config.py | 4 +- + virtwho/parser.py | 33 +- + virtwho/virt/ahv/__init__.py | 6 - + virtwho/virt/ahv/ahv.py | 258 ---------- + virtwho/virt/ahv/ahv_constants.py | 21 - + virtwho/virt/ahv/ahv_interface.py | 804 ------------------------------ + virtwho/virt/virt.py | 1 - + 8 files changed, 4 insertions(+), 1689 deletions(-) + delete mode 100644 tests/test_ahv.py + delete mode 100644 virtwho/virt/ahv/__init__.py + delete mode 100644 virtwho/virt/ahv/ahv.py + delete mode 100644 virtwho/virt/ahv/ahv_constants.py + delete mode 100644 virtwho/virt/ahv/ahv_interface.py + +diff --git a/tests/test_ahv.py b/tests/test_ahv.py +deleted file mode 100644 +index d8ccbc8..0000000 +--- a/tests/test_ahv.py ++++ /dev/null +@@ -1,566 +0,0 @@ +-from __future__ import print_function +- +-import six +- +-from base import TestBase +-from mock import patch, call, ANY, MagicMock +-from requests import Session +-from six.moves.queue import Queue +-from threading import Event +- +-from virtwho import DefaultInterval +-from virtwho.datastore import Datastore +-from virtwho.virt.ahv.ahv import AhvConfigSection +-from virtwho.virt import Virt, VirtError, Guest, Hypervisor +- +- +- +-MY_SECTION_NAME = 'test-ahv' +-DefaultUpdateInterval = 1800 +-# Values used for testing AhvConfigSection. +-PE_SECTION_VALUES = { +- 'type': 'ahv', +- 'server': '10.10.10.10', +- 'username': 'root', +- 'password': 'root_password', +- 'owner': 'nutanix', +- 'hypervisor_id': 'uuid', +- 'is_hypervisor': True, +- 'internal_debug': False, +- 'update_interval': 60 +-} +- +-HOST_UVM_MAP = \ +- {u'08469de5-be42-43e6-8c32-20167d3b58f7': +- {u'oplog_disk_pct': 3.4, +- u'memory_capacity_in_bytes': 135009402880, +- u'has_csr': False, +- u'default_vm_storage_container_uuid': None, +- u'hypervisor_username': u'root', +- u'key_management_device_to_certificate_status': {}, +- u'service_vmnat_ip': None, +- u'hypervisor_key': u'10.53.97.188', +- u'acropolis_connection_state': u'kConnected', +- u'management_server_name': u'10.53.97.188', +- u'failover_cluster_fqdn': None, +- u'serial': u'OM155S016008', +- u'bmc_version': u'01.92', +- u'hba_firmwares_list': +- [{u'hba_model': u'LSI Logic SAS3008', +- u'hba_version': u'MPTFW-06.00.00.00-IT'}], +- u'hypervisor_state': u'kAcropolisNormal', +- u'num_cpu_threads': 32, +- u'monitored': True, +- u'uuid': u'08469de5-be42-43e6-8c32-20167d3b58f7', +- u'reboot_pending': False, +- u'cpu_capacity_in_hz': 38384000000, +- u'num_cpu_sockets': 2, +- u'host_maintenance_mode_reason': None, +- u'hypervisor_address': u'10.53.97.188', +- u'host_gpus': None, +- u'failover_cluster_node_state': None, +- u'state': u'NORMAL', +- u'num_cpu_cores': 16, +- 'guest_list': +- [{u'vm_features': +- {u'AGENT_VM': False, +- u'VGA_CONSOLE': True}, +- u'name': u'am2', u'num_cores_per_vcpu': 1, +- u'gpus_assigned': False, u'num_vcpus': 2, +- u'memory_mb': 4096, +- u'power_state': u'on', +- u'ha_priority': 0, +- u'allow_live_migrate': True, +- u'timezone': u'America/Los_Angeles', +- u'vm_logical_timestamp': 48, +- u'host_uuid': u'08469de5-be42-43e6-8c32-20167d3b58f7', +- u'uuid': u'01dcfc0b-3092-4f1b-94fb-81b44ed352be'}, +- {u'vm_features': +- {u'AGENT_VM': False, u'VGA_CONSOLE': True}, +- u'name': u'am3', +- u'num_cores_per_vcpu': 1, +- u'gpus_assigned': False, +- u'num_vcpus': 2, u'memory_mb': 4096, +- u'power_state': u'on', +- u'ha_priority': 0, +- u'allow_live_migrate': True, +- u'timezone': u'America/Los_Angeles', +- u'vm_logical_timestamp': 3, +- u'host_uuid': u'08469de5-be42-43e6-8c32-20167d3b58f7', +- u'uuid': u'422f9171-db1f-48b0-a3de-b0bb92a8f559'}, +- {u'vm_features': +- {u'AGENT_VM': False, +- u'VGA_CONSOLE': True}, +- u'name': u'win_vm', +- u'num_cores_per_vcpu': 1, +- u'gpus_assigned': False, +- u'num_vcpus': 2, +- u'memory_mb': 4096, +- u'power_state': u'on', +- u'ha_priority': 0, +- u'allow_live_migrate': True, +- u'timezone': u'America/Los_Angeles', +- u'vm_logical_timestamp': 3, +- u'host_uuid': u'08469de5-be42-43e6-8c32-20167d3b58f7', +- u'uuid': u'98839f35-bd62-4255-a7cd-7668bc143554'}], +- u'cpu_model': u'Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz', +- u'ipmi_username': u'ADMIN', +- u'service_vmid': u'0005809e-62e4-75c7-611b-0cc47ac3b354::7', +- u'bmc_model': u'X10_ATEN', +- u'host_nic_ids': [], +- u'cluster_uuid': u'0005809e-62e4-75c7-611b-0cc47ac3b354', +- u'ipmi_password': None, +- u'cpu_frequency_in_hz': 2399000000, +- u'stats': +- {u'num_read_io': u'8', +- u'controller_read_io_bandwidth_kBps': u'0', +- u'content_cache_hit_ppm': u'1000000', +- }, +- u'num_vms': 4, u'default_vm_storage_container_id': None, +- u'metadata_store_status': u'kNormalMode', +- u'name': u'foyt-4', u'hypervisor_password': None, +- u'service_vmnat_port': None, +- u'hypervisor_full_name': u'Nutanix 20180802.100874', +- u'is_degraded': False, u'host_type': u'HYPER_CONVERGED', +- u'default_vhd_storage_container_uuid': None, +- u'block_serial': u'15SM60250038', +- u'disk_hardware_configs': +- {u'1': +- { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/BTHC506101XL480MGN', +- }, +- u'3': +- { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG8E6QE', +- }, +- u'2': +- { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/BTHC50610246480MGN', +- }, +- u'5': +- { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG8E835', +- }, +- u'4': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG8E8B1', +- }, +- u'6': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG8E7B3', +- }}, +- u'ipmi_address': u'10.49.27.28', u'bios_model': u'0824', +- u'default_vm_location': None, u'hypervisor_type': u'kKvm', +- u'service_vmexternal_ip': u'10.53.97.192', +- u'controller_vm_backplane_ip': u'10.53.97.192'}, +- u'54830446-b55e-4f16-aa74-7b6a9ac9a7a4': +- {u'oplog_disk_pct': 3.4, +- u'memory_capacity_in_bytes': 135009402880, u'has_csr': False, +- u'default_vm_storage_container_uuid': None, +- u'hypervisor_username': u'root', +- u'service_vmnat_ip': None, u'hypervisor_key': u'10.53.97.187', +- u'acropolis_connection_state': u'kConnected', +- u'hypervisor_state': u'kAcropolisNormal', +- u'num_cpu_threads': 32, u'monitored': True, +- u'uuid': u'54830446-b55e-4f16-aa74-7b6a9ac9a7a4', +- u'reboot_pending': False, u'cpu_capacity_in_hz': 38384000000, +- u'num_cpu_sockets': 2, u'host_maintenance_mode_reason': None, +- u'hypervisor_address': u'10.53.97.187', u'host_gpus': None, +- u'failover_cluster_node_state': None, u'state': u'NORMAL', +- u'num_cpu_cores': 16, u'block_model': u'UseLayout', +- 'guest_list': +- [{u'vm_features': +- {u'AGENT_VM': False, u'VGA_CONSOLE': True}, +- u'name': u'PC', u'num_cores_per_vcpu': 1, +- u'gpus_assigned': False, u'num_vcpus': 4, +- u'memory_mb': 16384, u'power_state': u'on', +- u'ha_priority': 0, u'allow_live_migrate': True, +- u'timezone': u'UTC', u'vm_logical_timestamp': 10, +- u'host_uuid': u'54830446-b55e-4f16-aa74-7b6a9ac9a7a4', +- u'uuid': u'd90b5443-97f0-47eb-986d-f14e062448d4'}, +- {u'vm_features': +- {u'AGENT_VM': False, u'VGA_CONSOLE': True}, +- u'name': u'am1', u'num_cores_per_vcpu': 1, +- u'gpus_assigned': False, u'num_vcpus': 2, +- u'memory_mb': 4096, u'power_state': u'on', +- u'ha_priority': 0, u'allow_live_migrate': True, +- u'timezone': u'America/Los_Angeles', +- u'vm_logical_timestamp': 14, +- u'host_uuid': u'54830446-b55e-4f16-aa74-7b6a9ac9a7a4', +- u'uuid': u'0af0a010-0ad0-4fba-aa33-7cc3d0b6cb7e'}], +- u'cpu_model': u'Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz', +- u'ipmi_username': u'ADMIN', +- u'service_vmid': u'0005809e-62e4-75c7-611b-0cc47ac3b354::6', +- u'bmc_model': u'X10_ATEN', u'host_nic_ids': [], +- u'cluster_uuid': u'0005809e-62e4-75c7-611b-0cc47ac3b354', +- u'stats': +- {u'num_read_io': u'27', +- u'controller_read_io_bandwidth_kBps': u'0', +- u'content_cache_hit_ppm': u'1000000', +- }, u'backplane_ip': None, +- u'vzone_name': u'', u'default_vhd_location': None, +- u'metadata_store_status_message': u'Metadata store enabled on the node', +- u'num_vms': 3, u'default_vm_storage_container_id': None, +- u'metadata_store_status': u'kNormalMode', u'name': u'foyt-3', +- u'hypervisor_password': None, u'service_vmnat_port': None, +- u'hypervisor_full_name': u'Nutanix 20180802.100874', +- u'is_degraded': False, u'host_type': u'HYPER_CONVERGED', +- u'default_vhd_storage_container_uuid': None, +- u'block_serial': u'15SM60250038', +- u'disk_hardware_configs': +- {u'1': +- { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/BTHC506101ST480MGN', +- }, +- u'3': +- { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG8DXYY', +- }, +- u'2': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/BTHC506101D4480MGN', +- }, +- u'5': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG8DQRM', +- }, +- u'4': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG8DJ7E', +- }, +- u'6': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG8DVGG', +- }}, +- u'ipmi_address': u'10.49.27.27', u'bios_model': u'0824', +- u'hypervisor_type': u'kKvm', +- u'service_vmexternal_ip': u'10.53.97.191', +- u'controller_vm_backplane_ip': u'10.53.97.191' +- }, +- u'acc819fe-e0ff-4963-93a4-5a0e1d3c77d3': +- {u'oplog_disk_pct': 3.4, +- u'memory_capacity_in_bytes': 270302969856, u'has_csr': False, +- u'default_vm_storage_container_uuid': None, +- u'hypervisor_username': u'root', +- u'key_management_device_to_certificate_status': {}, +- u'service_vmnat_ip': None, u'hypervisor_key': u'10.53.96.75', +- u'acropolis_connection_state': u'kConnected', +- u'management_server_name': u'10.53.96.75', +- u'failover_cluster_fqdn': None, u'serial': u'ZM162S002621', +- u'bmc_version': u'01.97', +- u'hba_firmwares_list': +- [{u'hba_model': u'LSI Logic SAS3008', +- u'hba_version': u'MPTFW-10.00.03.00-IT'}], +- u'hypervisor_state': u'kAcropolisNormal', +- u'num_cpu_threads': 32, u'monitored': True, +- u'uuid': u'acc819fe-e0ff-4963-93a4-5a0e1d3c77d3', +- u'num_cpu_sockets': 2, u'host_maintenance_mode_reason': None, +- u'hypervisor_address': u'10.53.96.75', +- 'guest_list': +- [{u'vm_features': {u'AGENT_VM': False, u'VGA_CONSOLE': True}, +- u'name': u'am_RH_satellite', u'num_cores_per_vcpu': 2, +- u'gpus_assigned': False, u'num_vcpus': 4, u'memory_mb': 16384, +- u'power_state': u'on', u'ha_priority': 0, +- u'allow_live_migrate': True, u'timezone': u'America/Los_Angeles', +- u'vm_logical_timestamp': 5, +- u'host_uuid': u'acc819fe-e0ff-4963-93a4-5a0e1d3c77d3', +- u'uuid': u'e30f381d-d4bc-4958-a88c-79448efe5112'}, +- {u'vm_features': {u'AGENT_VM': False, u'VGA_CONSOLE': True}, +- u'name': u'am4', u'num_cores_per_vcpu': 1, +- u'gpus_assigned': False, u'num_vcpus': 2, u'memory_mb': 4096, +- u'power_state': u'on', u'ha_priority': 0, +- u'allow_live_migrate': True, u'timezone': u'America/Los_Angeles', +- u'vm_logical_timestamp': 2, +- u'host_uuid': u'acc819fe-e0ff-4963-93a4-5a0e1d3c77d3', +- u'uuid': u'f1e3362b-0377-4d70-bccd-63d2a1c09225'}], +- u'dynamic_ring_changing_node': None, +- u'cpu_model': u'Intel(R) Xeon(R) CPU E5-2630 v3 @ 2.40GHz', +- u'ipmi_username': u'ADMIN', +- u'cluster_uuid': u'0005809e-62e4-75c7-611b-0cc47ac3b354', +- u'ipmi_password': None, u'cpu_frequency_in_hz': 2400000000, +- u'stats': {u'num_read_io': u'47', +- u'controller_read_io_bandwidth_kBps': u'0', +- u'content_cache_hit_ppm': u'1000000', +- }, u'backplane_ip': None, +- u'num_vms': 3, +- u'name': u'watermelon02-4', u'hypervisor_password': None, +- u'hypervisor_full_name': u'Nutanix 20180802.100874', +- u'is_degraded': False, u'host_type': u'HYPER_CONVERGED', +- u'default_vhd_storage_container_uuid': None, +- u'block_serial': u'16AP60170033', u'usage_stats': { +- }, +- u'disk_hardware_configs': { +- u'1': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/BTHC549209M3480MGN', +- }, +- u'3': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG9TRZQ'}, +- u'2': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/BTHC550503XF480MGN', +- }, +- u'5': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG9TS0N', +- }, +- u'4': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG9TSF7', +- }, +- u'6': { +- u'mount_path': u'/home/nutanix/data/stargate-storage/disks/9XG9TREW', +- }}, u'ipmi_address': u'10.49.26.188', +- u'bios_model': u'0824', u'default_vm_location': None, +- u'hypervisor_type': u'kKvm', +- u'position': {u'ordinal': 4, u'physical_position': None, +- u'name': u''}, +- u'service_vmexternal_ip': u'10.53.96.79', +- u'controller_vm_backplane_ip': u'10.53.96.79'}} +- +- +-class TestAhvConfigSection(TestBase): +- """ +- Test base for testing class AhvConfigSection. +- """ +- +- def __init__(self, *args, **kwargs): +- super(TestAhvConfigSection, self).__init__(*args, **kwargs) +- self.ahv_config = None +- +- def init_virt_config_section(self, is_pc=False): +- """ +- Method executed before each unit test. +- """ +- self.ahv_config = AhvConfigSection(MY_SECTION_NAME, None) +- if is_pc: +- self.ahv_config['prism_central'] = True +- # We need to set values using this way, because we need +- # to trigger __setitem__ of virt_config. +- for key, value in PE_SECTION_VALUES.items(): +- self.ahv_config[key] = value +- +- def test_validate_ahv_PE_config(self): +- """ +- Test validation of ahv section. +- """ +- # PE validation. +- self.init_virt_config_section() +- result = self.ahv_config.validate() +- self.assertEqual(len(result), 0) +- +- # PC validation. +- self.init_virt_config_section(is_pc=True) +- result = self.ahv_config.validate() +- self.assertEqual(len(result), 0) +- +- def test_validate_ahv_invalid_server_ip(self): +- """ +- Test validation of ahv config. Invalid server IP. +- """ +- self.init_virt_config_section() +- self.ahv_config['server'] = '10.0.0.' +- result = self.ahv_config.validate() +- expected_result = ['Invalid server IP address provided'] +- six.assertCountEqual(self, expected_result, result) +- +- def test_validate_ahv_config_missing_username_password(self): +- """ +- Test validation of ahv config. Username and password is required. +- """ +- self.init_virt_config_section() +- del self.ahv_config['username'] +- del self.ahv_config['password'] +- result = self.ahv_config.validate() +- expected_result = [ +- ('error', 'Required option: "username" not set.'), +- ('error', 'Required option: "password" not set.') +- ] +- six.assertCountEqual(self, expected_result, result) +- +- def test_validate_ahv_config_invalid_internal_debug_flag(self): +- """ +- Test validation of ahv config. If update_interval and internal debug +- are not set then we get a warning message for each flag. +- """ +- self.init_virt_config_section() +- self.ahv_config['update_interval'] = 40 +- result = self.ahv_config.validate() +- message = "Interval value can't be lower than {min} seconds. " \ +- "Default value of {min} " \ +- "seconds will be used.".format(min=DefaultUpdateInterval) +- expected_result = [("warning", message)] +- six.assertCountEqual(self, expected_result, result) +- +- +-class TestAhv(TestBase): +- +- @staticmethod +- def create_config(name, wrapper, **kwargs): +- config = AhvConfigSection(name, wrapper) +- config.update(**kwargs) +- config.validate() +- return config +- +- def setUp(self, is_pc=False): +- config = self.create_config(name='test', wrapper=None, type='ahv', +- server='10.10.10.10', username='username', +- password='password', owner='owner', +- prism_central=is_pc) +- self.ahv = Virt.from_config(self.logger, config, Datastore(), +- interval=DefaultInterval) +- +- @patch('virtwho.virt.ahv.ahv_interface.AhvInterface._progressbar') +- def run_once(self, queue=None): +- """Run AHV in oneshot mode.""" +- self.ahv._oneshot = True +- self.ahv.dest = queue or Queue() +- self.ahv._terminate_event = Event() +- self.ahv._oneshot = True +- self.ahv._interval = 0 +- self.ahv._run() +- +- @patch.object(Session, 'get') +- def test_connect_PE(self, mock_get): +- mock_get.return_value.status_code = 200 +- self.run_once() +- +- self.assertEqual(mock_get.call_count, 3) +- call_list = [ +- call('https://10.10.10.10:9440/api/nutanix/v2.0/clusters', +- data=ANY, headers=ANY, timeout=ANY, verify=ANY), +- call('https://10.10.10.10:9440/api/nutanix/v2.0/vms', +- data=ANY, headers=ANY, timeout=ANY, verify=ANY), +- call('https://10.10.10.10:9440/api/nutanix/v2.0/hosts', +- data=ANY, headers=ANY, timeout=ANY, verify=ANY) +- ] +- mock_get.assert_has_calls(call_list, any_order=True) +- +- @patch.object(Session, 'post') +- def test_connect_PC(self, mock_post): +- self.setUp(is_pc=True) +- +- mock_post.return_value.status_code = 200 +- self.run_once() +- +- self.assertEqual(mock_post.call_count, 3) +- call_list = [ +- call('https://10.10.10.10:9440/api/nutanix/v3/clusters/list', +- data=ANY, headers=ANY, timeout=ANY, verify=ANY), +- call('https://10.10.10.10:9440/api/nutanix/v3/vms/list', +- data=ANY, headers=ANY, timeout=ANY, verify=ANY), +- call('https://10.10.10.10:9440/api/nutanix/v3/hosts/list', +- data=ANY, headers=ANY, timeout=ANY, verify=ANY) +- ] +- mock_post.assert_has_calls(call_list, any_order=True) +- +- @patch.object(Session, 'get') +- def test_invalid_login_PE(self, mock_get): +- mock_get.return_value.ok = False +- mock_get.return_value.status_code = 401 +- self.assertRaises(VirtError, self.run_once) +- +- mock_get.return_value.status_code = 403 +- self.assertRaises(VirtError, self.run_once) +- +- @patch.object(Session, 'post') +- def test_invalid_login_PC(self, mock_post): +- self.setUp(is_pc=True) +- mock_post.return_value.ok = False +- mock_post.return_value.status_code = 401 +- self.assertRaises(VirtError, self.run_once) +- +- mock_post.return_value.status_code = 403 +- self.assertRaises(VirtError, self.run_once) +- +- @patch.object(Session, 'get') +- def test_connection_conflict_PE(self, mock_get): +- mock_get.return_value.ok = False +- mock_get.return_value.status_code = 409 +- self.assertRaises(VirtError, self.run_once) +- +- @patch.object(Session, 'post') +- def test_connection_conflict_PC(self, mock_post): +- self.setUp(is_pc=True) +- mock_post.return_value.ok = False +- mock_post.return_value.status_code = 409 +- self.assertRaises(VirtError, self.run_once) +- +- @patch('virtwho.virt.ahv.ahv_interface.AhvInterface.get_vm', return_value=None) +- @patch.object(Session, 'get') +- def test_no_retry_http_erros_PE(self, mock_get, mock_get_vm): +- mock_get.return_value.ok = False +- mock_get.return_value.status_code = 400 +- mock_get.return_value.text = 'Bad Request' +- self.assertEqual(mock_get_vm.return_value, None) +- +- mock_get.return_value.status_code = 404 +- mock_get.return_value.text = 'Not Found Error' +- self.assertEqual(mock_get_vm.return_value, None) +- +- mock_get.return_value.status_code = 500 +- mock_get.return_value.text = 'Internal Server Error' +- self.assertEqual(mock_get_vm.return_value, None) +- +- mock_get.return_value.status_code = 502 +- mock_get.return_value.tex = 'Bad Gateway' +- self.assertEqual(mock_get_vm.return_value, None) +- +- mock_get.return_value.status_code = 503 +- mock_get.return_value.text = 'Service Unavailable ' +- self.assertEqual(mock_get_vm.return_value, None) +- +- @patch('virtwho.virt.ahv.ahv_interface.AhvInterface.get_vm', return_value=None) +- @patch.object(Session, 'post') +- def test_no_retry_http_erros_PC(self, mock_post, mock_get_vm): +- self.setUp(is_pc=True) +- mock_post.return_value.ok = False +- mock_post.return_value.status_code = 400 +- mock_post.return_value.text = 'Bad Request' +- self.assertEqual(mock_get_vm.return_value, None) +- +- mock_post.return_value.status_code = 404 +- mock_post.return_value.text = 'Not Found Error' +- self.assertEqual(mock_get_vm.return_value, None) +- +- mock_post.return_value.status_code = 500 +- mock_post.return_value.text = 'Internal Server Error' +- self.assertEqual(mock_get_vm.return_value, None) +- +- mock_post.return_value.status_code = 502 +- mock_post.return_value.tex = 'Bad Gateway' +- self.assertEqual(mock_get_vm.return_value, None) +- +- mock_post.return_value.status_code = 503 +- mock_post.return_value.text = 'Service Unavailable ' +- self.assertEqual(mock_get_vm.return_value, None) +- +- @patch('virtwho.virt.ahv.ahv_interface.AhvInterface.build_host_to_uvm_map') +- def test_getHostGuestMapping(self, host_to_uvm_map): +- host_to_uvm_map.return_value = HOST_UVM_MAP +- +- expected_result = [] +- +- for host_uuid in HOST_UVM_MAP: +- host = HOST_UVM_MAP[host_uuid] +- hypervisor_id = host_uuid +- host_name = host['name'] +- cluster_uuid = host['cluster_uuid'] +- guests = [] +- for guest_vm in host['guest_list']: +- state = guest_vm['power_state'] +- guests.append(Guest(guest_vm['uuid'], self.ahv.CONFIG_TYPE, +- state)) +- +- facts = { +- Hypervisor.CPU_SOCKET_FACT: '2', +- Hypervisor.HYPERVISOR_TYPE_FACT: u'kKvm', +- Hypervisor.HYPERVISOR_VERSION_FACT: 'Nutanix 20180802.100874', +- Hypervisor.HYPERVISOR_CLUSTER: str(cluster_uuid) +- } +- +- expected_result.append(Hypervisor( +- name=host_name, +- hypervisorId=hypervisor_id, +- guestIds=guests, +- facts=facts +- )) +- +- result = self.ahv.getHostGuestMapping()['hypervisors'] +- +- self.assertEqual(len(result), len(expected_result), 'lists length ' +- 'do not match') +- for index in range(0, len(result)): +- self.assertEqual(expected_result[index].toDict(), +- result[index].toDict()) +- +diff --git a/virtwho/config.py b/virtwho/config.py +index b9f3f2e..9064ede 100644 +--- a/virtwho/config.py ++++ b/virtwho/config.py +@@ -50,7 +50,7 @@ logger = log.getLogger(name='config', queue=False) + _effective_config = None + + VW_CONF_DIR = "/etc/virt-who.d/" +-VW_TYPES = ("libvirt", "esx", "rhevm", "hyperv", "fake", "xen", "kubevirt", "ahv") ++VW_TYPES = ("libvirt", "esx", "rhevm", "hyperv", "fake", "xen", "kubevirt") + VW_GENERAL_CONF_PATH = "/etc/virt-who.conf" + VW_GLOBAL = "global" + VW_VIRT_DEFAULTS_SECTION_NAME = "defaults" +@@ -1150,7 +1150,7 @@ class VirtConfigSection(ConfigSection): + result = None + sm_type = self._values['sm_type'] + virt_type = self._values.get('type') +- if sm_type == 'sam' and virt_type in ('esx', 'rhevm', 'hyperv', 'xen', 'ahv'): ++ if sm_type == 'sam' and virt_type in ('esx', 'rhevm', 'hyperv', 'xen'): + if key not in self: + result = ( + 'warning', +diff --git a/virtwho/parser.py b/virtwho/parser.py +index eeb7aad..b7dcbd5 100644 +--- a/virtwho/parser.py ++++ b/virtwho/parser.py +@@ -44,7 +44,6 @@ SAT5_VM_DISPATCHER = { + 'rhevm': {'owner': False, 'server': True, 'username': True}, + 'hyperv': {'owner': False, 'server': True, 'username': True}, + 'kubevirt': {'owner': False, 'server': False, 'username': False, 'kubeconfig': True, 'kubeversion': False}, +- 'ahv' : {'owner': False, 'server': False, 'username': False}, + } + + SAT6_VM_DISPATCHER = { +@@ -54,7 +53,6 @@ SAT6_VM_DISPATCHER = { + 'rhevm': {'owner': True, 'server': True, 'username': True}, + 'hyperv': {'owner': True, 'server': True, 'username': True}, + 'kubevirt': {'owner': True, 'server': False, 'username': False, 'kubeconfig': True, 'kubeversion': False}, +- 'ahv' : {'owner': False, 'server': False, 'username': False}, + } + + class OptionError(Exception): +@@ -72,7 +70,7 @@ class StoreGroupArgument(Action): + def __call__(self, parser, namespace, values, option_string=None): + """ + When the argument from group is used, then this argument has to match +- virtualization backend [--libvirt|--esx|--rhevm|--hyperv|--xen|--kubevirt|--ahv] ++ virtualization backend [--libvirt|--esx|--rhevm|--hyperv|--xen|--kubevirt] + """ + options = vars(namespace) + virt_type = options['virt_type'] +@@ -207,7 +205,6 @@ def read_config_env_variables(): + "VIRTWHO_RHEVM": ("virt_type", store_const, "rhevm"), + "VIRTWHO_HYPERV": ("virt_type", store_const, "hyperv"), + "VIRTWHO_KUBEVIRT": ("virt_type", store_const, "kubevirt"), +- "VIRTWHO_AHV": ("virt_type", store_const, "ahv"), + "VIRTWHO_INTERVAL": ("interval", store_value), + "VIRTWHO_REPORTER_ID": ("reporter_id", store_value), + } +@@ -306,7 +303,7 @@ def parse_cli_arguments(): + parser = ArgumentParser( + usage="virt-who [-d] [-o] [-i INTERVAL] [-p] [-c CONFIGS] [--version] " + "[-m] [-l LOG_DIR] [-f LOG_FILE] [-r REPORTER_ID] [--sam|--satellite5|--satellite6] " +- "[--libvirt|--esx|--rhevm|--hyperv|--xen|--kubevirt|--ahv]", ++ "[--libvirt|--esx|--rhevm|--hyperv|--xen|--kubevirt]", + description="Agent for reporting virtual guest IDs to subscription manager", + epilog="virt-who also reads environment variables. They have the same name as " + "command line arguments but uppercased, with underscore instead of dash " +@@ -365,8 +362,6 @@ def parse_cli_arguments(): + help="[Deprecated] Register guests using Hyper-V") + virt_group.add_argument("--kubevirt", action=StoreVirtType, dest="virt_type", const="kubevirt", + help="[Deprecated] Register guests using Kubevirt") +- virt_group.add_argument("--ahv", action=StoreVirtType, dest="virt_type", const="ahv", +- default=None, help="[Deprecated] Register Acropolis vms using AHV.") + + manager_group = parser.add_argument_group( + title="Subscription manager", +@@ -485,30 +480,6 @@ def parse_cli_arguments(): + kubevirt_group.add_argument("--kubevirt-cfg", action=StoreGroupArgument, dest="kubeconfig", default="~/.kube/config", + help="[Deprecated] Path to Kubernetes config file") + +- ahv_group = parser.add_argument_group( +- title="AHV PC/PE options", +- description="Use these options with --ahv" +- ) +- ahv_group.add_argument("--ahv-owner", action=StoreGroupArgument, dest="owner", default="", +- help="[Deprecated] Organization who has purchased subscriptions of the products") +- ahv_group.add_argument("--ahv-env", action=StoreGroupArgument, dest="env", default="", +- help="[Deprecated] Environment where the vCenter server belongs to") +- ahv_group.add_argument("--ahv-server", action=StoreGroupArgument, +- dest="server", default="", +- help="[Deprecated] URL of the ahv server to connect to") +- ahv_group.add_argument("--ahv-username", action=StoreGroupArgument, +- dest="username", default="", +- help="[Deprecated] Username for connecting to ahv server") +- ahv_group.add_argument("--ahv-password", action=StoreGroupArgument, +- dest="password", default="", +- help="[Deprecated] Password for connecting to ahv server") +- ahv_group.add_argument("--pc-server", action=StoreGroupArgument, dest="server", default="", +- help="[Deprecated] URL of the PC server to connect to") +- ahv_group.add_argument("--pc-username", action=StoreGroupArgument, dest="username", default="", +- help="[Deprecated] Username for connecting to PC") +- ahv_group.add_argument("--pc-password", action=StoreGroupArgument, dest="password", default="", +- help="[Deprecated] Password for connecting to PC") +- + # Read option from CLI + cli_options = vars(parser.parse_args()) + +diff --git a/virtwho/virt/ahv/__init__.py b/virtwho/virt/ahv/__init__.py +deleted file mode 100644 +index 01a19f8..0000000 +--- a/virtwho/virt/ahv/__init__.py ++++ /dev/null +@@ -1,6 +0,0 @@ +-# -*- coding: utf-8 -*- +-from __future__ import absolute_import, print_function +- +-from .ahv import Ahv +- +-__all__ = ['Ahv'] +diff --git a/virtwho/virt/ahv/ahv.py b/virtwho/virt/ahv/ahv.py +deleted file mode 100644 +index 47a786d..0000000 +--- a/virtwho/virt/ahv/ahv.py ++++ /dev/null +@@ -1,258 +0,0 @@ +-import socket +- +-from . import ahv_constants +-from .ahv_interface import AhvInterface, Failure +-from time import time +-from virtwho import virt +-from virtwho.config import VirtConfigSection +-from virtwho.virt import Hypervisor, Guest +- +-DefaultUpdateInterval = 1800 +-MinimumUpdateInterval = 60 +- +-class Ahv(virt.Virt): +- "AHV Rest client" +- CONFIG_TYPE = "ahv" +- def __init__(self, logger, config, dest, interval=None, +- terminate_event=None, oneshot=False): +- """ +- Args: +- logger (Logger): Framework logger. +- config (onfigSection): Virtwho configuration. +- dest (Datastore): Data store for destination. +- interval (Int): Wait interval for continuous run. +- terminate_event (Event): Event on termination. +- one_shot (bool): Flag to run virtwho as onetime or continuously. +- Returns: +- None. +- """ +- super(Ahv, self).__init__(logger, config, dest, +- terminate_event=terminate_event, +- interval=interval, +- oneshot=oneshot) +- self.config = config +- self.version = ahv_constants.VERSION_2 +- self.is_pc = False +- if 'prism_central' in self.config: +- if self.config['prism_central']: +- self.version = ahv_constants.VERSION_3 +- self.is_pc = True +- +- self.port = ahv_constants.DEFAULT_PORT +- self.url = ahv_constants.SERVER_BASE_URIL % (self.config['server'], +- self.port, self.version) +- self.port = ahv_constants.DEFAULT_PORT +- self.username = self.config['username'] +- self.password = self.config['password'] +- self.update_interval = self.config['update_interval'] +- self._interface = AhvInterface(logger, self.url, self.username, +- self.password, self.port, +- internal_debug=self.config['internal_debug']) +- +- def prepare(self): +- """ +- Prepare for obtaining information from AHV server. +- Args: +- None +- Returns: +- None +- """ +- self.logger.debug("Logging into Acropolis server %s" % self.url) +- self._interface.login(self.version) +- +- def _wait_for_update(self, timeout): +- """ +- Wait for an update from AHV. +- Args: +- timeout (int): timeout +- Returns: +- task list (list): List of vm or host related tasks. +- """ +- try: +- end_time = time() + timeout +- timestamp = int(time() * 1e6) +- while time() < end_time and not self.is_terminated(): +- try: +- response = self._interface.get_tasks(timestamp, self.version, +- self.is_pc) +- if len(response) == 0: +- # No events, continue to wait +- continue +- self.logger.debug('AHV event found: %s\n' % response) +- return response +- except Failure as e: +- if 'timeout' not in e.details: +- raise +- except Exception: +- self.logger.exception("Waiting on AHV events failed: ") +- +- return [] +- +- def getHostGuestMapping(self): +- """ +- Get a dict of host to uvm mapping. +- Args: +- None. +- Returns: +- None. +- """ +- mapping = {'hypervisors': []} +- +- host_uvm_map = self._interface.build_host_to_uvm_map(self.version) +- +- for host_uuid in host_uvm_map: +- host = host_uvm_map[host_uuid] +- +- try: +- if self.config['hypervisor_id'] == 'uuid': +- hypervisor_id = host_uuid +- elif self.config['hypervisor_id'] == 'hostname': +- hypervisor_id = host['name'] +- +- except KeyError: +- self.logger.debug("Host '%s' doesn't have hypervisor_id property", +- host_uuid) +- continue +- +- guests = [] +- if 'guest_list' in host and len(host['guest_list']) > 0: +- for guest_vm in host['guest_list']: +- try: +- state = guest_vm['power_state'] +- except KeyError: +- self.logger.warning("Guest %s is missing power state. Perhaps they" +- " are powered off", guest_vm['uuid']) +- continue +- guests.append(Guest(guest_vm['uuid'], self.CONFIG_TYPE, state)) +- else: +- self.logger.debug("Host '%s' doesn't have any vms", host_uuid) +- +- cluster_uuid = self._interface.get_host_cluster_uuid(host) +- host_version = self._interface.get_host_version(host) +- host_name = host['name'] +- +- facts = { +- Hypervisor.CPU_SOCKET_FACT: str(host['num_cpu_sockets']), +- Hypervisor.HYPERVISOR_TYPE_FACT: host.get('hypervisor_type', 'AHV'), +- Hypervisor.HYPERVISOR_VERSION_FACT: str(host_version), +- Hypervisor.HYPERVISOR_CLUSTER: str(cluster_uuid)} +- +- mapping['hypervisors'].append(virt.Hypervisor(hypervisorId=hypervisor_id, +- guestIds=guests, +- name=host_name, +- facts=facts)) +- return mapping +- +- def _run(self): +- """ +- Continuous run loop for virt-who on AHV. +- Args: +- None. +- Returns: +- None. +- """ +- self.prepare() +- next_update = time() +- initial = True +- wait_result = None +- while self._oneshot or not self.is_terminated(): +- +- delta = next_update - time() +- +- if initial: +- assoc = self.getHostGuestMapping() +- self._send_data(virt.HostGuestAssociationReport(self.config, assoc)) +- initial = False +- continue +- +- if delta > 0: +- # Wait for update. +- wait_result = self._wait_for_update(60 if initial else delta) +- if wait_result: +- events = wait_result +- else: +- events = [] +- else: +- events = [] +- +- if len(events) > 0 or delta > 0: +- assoc = self.getHostGuestMapping() +- self._send_data(virt.HostGuestAssociationReport(self.config, assoc)) +- +- if self._oneshot: +- break +- else: +- next_update = time() + self.update_interval +- +-class AhvConfigSection(VirtConfigSection): +- """Class for intializing and processing AHV config""" +- VIRT_TYPE = 'ahv' +- HYPERVISOR_ID = ('uuid', 'hwuuid', 'hostname') +- +- def __init__(self, *args, **kwargs): +- """ +- Initialize AHV config and add config keys. +- Args: +- args: args +- kwargs : kwargs +- Returns: +- None. +- """ +- super(AhvConfigSection, self).__init__(*args, **kwargs) +- self.add_key('server', validation_method=self._validate_server, +- required=True) +- self.add_key('username', validation_method=self._validate_username, +- required=True) +- self.add_key('password', +- validation_method=self._validate_unencrypted_password, +- required=True) +- self.add_key('is_hypervisor', validation_method=self._validate_str_to_bool, +- default=True) +- self.add_key('prism_central', validation_method=self._validate_str_to_bool, +- default=None) +- self.add_key('internal_debug', validation_method=self._validate_str_to_bool, +- default=False) +- self.add_key('update_interval', +- validation_method=self._validate_update_interval, +- default=DefaultUpdateInterval) +- +- def _validate_server(self, key): +- """ +- Validate the server IP address. +- Args: +- key (Str): server Ip address. +- Returns: +- Socket error is returned in case of an invalid ip. +- """ +- error = super(AhvConfigSection, self)._validate_server(key) +- try: +- ip = self._values[key] +- socket.inet_aton(ip) +- except socket.error: +- error = 'Invalid server IP address provided' +- return error +- +- def _validate_update_interval(self, key): +- """ +- Validate the update internal flag. +- Args: +- key (Int): Update internal value. +- Returns: +- A warning is returned in case interval is not valid. +- """ +- result = None +- try: +- self._values[key] = int(self._values[key]) +- +- if self._values[key] < MinimumUpdateInterval: +- message = "Interval value can't be lower than {min} seconds. " \ +- "Default value of {min} " \ +- "seconds will be used.".format(min=DefaultUpdateInterval) +- result = ("warning", message) +- self._values['interval'] = DefaultUpdateInterval +- except KeyError: +- result = ('warning', '%s is missing' % key) +- except (TypeError, ValueError) as e: +- result = ( +- 'warning', '%s was not set to a valid integer: %s' % (key, str(e))) +- return result +diff --git a/virtwho/virt/ahv/ahv_constants.py b/virtwho/virt/ahv/ahv_constants.py +deleted file mode 100644 +index 95534a0..0000000 +--- a/virtwho/virt/ahv/ahv_constants.py ++++ /dev/null +@@ -1,21 +0,0 @@ +-SERVER_BASE_URIL = 'https://%s:%d/api/nutanix/%s' +-AHV_HYPERVIRSOR = ['kKvm', 'AHV', 'ahv', 'kvm'] +-TASK_COMPLETE_MSG = ['SUCCEEDED', 'Succeeded'] +-DEFAULT_PORT = 9440 +-VERSION_2 = 'v2.0' +-VERSION_3 = 'v3' +- +-CMN_RST_CMD = {'get_vm': {'url': '/vms/%s', 'method': 'get'}, +- 'get_host': {'url': '/hosts/%s', 'method': 'get'}, +- 'get_tasks': {'url': '/tasks/list', 'method': 'post'}, +- 'get_task': {'url': '/tasks/%s', 'method': 'get'}} +- +-REST_CMD = {VERSION_2: {'list_vms': {'url': '/vms', 'method': 'get'}, +- 'list_hosts': {'url': '/hosts', 'method': 'get'}, +- 'list_clusters' : {'url': '/clusters', +- 'method': 'get'}}, +- VERSION_3: {'list_vms': {'url': '/vms/list', 'method': 'post'}, +- 'list_hosts': {'url': '/hosts/list', 'method': 'post'}, +- 'list_clusters' : {'url': '/clusters/list', +- 'method': 'post'}}} +- +diff --git a/virtwho/virt/ahv/ahv_interface.py b/virtwho/virt/ahv/ahv_interface.py +deleted file mode 100644 +index 8b75cb6..0000000 +--- a/virtwho/virt/ahv/ahv_interface.py ++++ /dev/null +@@ -1,804 +0,0 @@ +-import json +-import math +-import time +-import sys +-from . import ahv_constants +-from requests import Session +-from requests.exceptions import ConnectionError, ReadTimeout +-from virtwho import virt +- +-class AhvInterface(object): +- """ AHV REST Api interface class""" +- NO_RETRY_HTTP_CODES = [400, 404, 500, 502, 503] +- event_types = ['node', 'vm'] +- +- def __init__(self, logger, url, username, password, port, **kwargs): +- """ +- Args: +- logger (Log): Logger. +- url (str): Rest server url. +- username (str): Username. +- password (str): Password for rest client. +- port (int): Port number for ssp. +- kwargs(dict): Accepts following arguments: +- timeout(optional, int): Max seconds to wait before HTTP connection +- times-out. Default 30 seconds. +- retries (optional, int): Maximum number of retires. Default: 5. +- retry_interval (optional, int): Time to sleep between retry intervals. +- internal_debug (optional, bool): Detail log of the rest calls. +- Default: 5 seconds. +- """ +- self._session = Session() +- self._timeout = kwargs.get('timeout', 30) +- self._retries = kwargs.get('retries', 5) +- self._retry_interval = kwargs.get('retry_interval', 30) +- self._logger = logger +- self._url = url +- self._user = username +- self._password = password +- self._port = port +- self._internal_debug = kwargs.get('internal_debug', False) +- self._create_session(self._user, self._password) +- +- def _create_session(self, user=None, password=None): +- """ +- Creates rest session. +- Args: +- user (str): Username. +- password (str): Password for rest session. +- Returns: +- None. +- """ +- if user is None: +- user = self._user +- if password is None: +- password = self._password +- self._session.auth = (user, password) +- +- def _make_url(self, uri, *args): +- """ +- Creates base url. +- uri would always begin with a slash +- Args: +- uri (str): Uri. +- args (list): Args. +- Returns: +- url (str): Url with uri. +- """ +- if not uri.startswith("/"): +- uri = "/%s" % uri +- url = "%s%s" % (self._url, uri) +- for arg in args: +- url += "/%s" % str(arg) +- return url +- +- def _format_response(self, data): +- """ +- Format the data based on the response's version. +- Args: +- data (dict): Data dictionary. +- Returns: +- formatted_data (dict): Formatted dictionary. +- """ +- if 'entities' in data: +- return self._process_entities_list(data['entities']) +- else: +- return self._process_dict_response(data) +- +- def _process_dict_response(self, data): +- """ +- Format the data when we only have a dictionary. +- Args: +- data (dict): Data dictionary. +- Returns: +- formatted_data (dict): Formatted data. +- """ +- formatted_data = data +- if 'status' in data and 'metadata' in data: +- formatted_data = dict(data['status'], **data['metadata']) +- +- if 'resources' in formatted_data: +- if 'power_state' in formatted_data['resources']: +- formatted_data['power_state'] = \ +- formatted_data['resources']['power_state'] +- if 'num_cpu_sockets' in formatted_data['resources']: +- formatted_data['num_cpu_sockets'] = \ +- formatted_data['resources']['num_cpu_sockets'] +- +- return formatted_data +- +- def _process_entities_list(self, data): +- """ +- Format data for the list of entities. +- Args: +- data (list): List of entities dictionary. +- Returns: +- formatted_data (dict): Formatted data after processing list fo entities. +- """ +- formatted_data = data +- initial = True +- for entity in data: +- if 'status' in entity and 'metadata' in entity: +- if initial: +- formatted_data = [] +- initial = False +- formatted_data.append(dict(entity['status'], **entity['metadata'])) +- +- for ent_obj in formatted_data: +- if 'resources' in ent_obj: +- if 'nodes' in ent_obj['resources']: +- nodes = ent_obj['resources']['nodes'] +- if 'hypervisor_server_list' in nodes: +- ent_obj['hypervisor_types'] = [] +- for server in nodes['hypervisor_server_list']: +- ent_obj['hypervisor_types'].append(server['type']) +- +- if 'kind' in ent_obj: +- if ent_obj['kind'] == 'cluster': +- if 'uuid' in ent_obj: +- ent_obj['cluster_uuid'] = ent_obj['uuid'] +- +- return formatted_data +- +- def _progressbar(self, it, prefix="", size=60, file=sys.stdout, total=0, is_pc=False): +- count = total +- cursor = 0 +- def show(j): +- x = int(size*j/count) +- file.write("%s[%s%s] %i/%i\r" % (prefix, "#"*x, "."*(size-x), j, count)) +- file.flush() +- show(0) +- +- for i, item in enumerate(it): +- if is_pc: +- yield item +- for i in range(20): +- show(cursor+1) +- cursor += 1 +- if cursor == count: +- break +- time.sleep(0.1) +- else: +- show(i+1) +- +- yield item +- file.write("\n") +- file.flush() +- +- def login(self, version): +- """ +- Login to the rest server and ensure connection succeeds. +- Args: +- version (Str): Interface version. +- Returns: +- None. +- """ +- (url, cmd_method) = self.get_diff_ver_url_and_method( +- cmd_key='list_clusters', intf_version=version) +- self.make_rest_call(method=cmd_method, uri=url) +- self._logger.info("Successfully logged into the AHV REST server") +- +- def get_hypervisor_type(self, version, host_entity=None, vm_entity=None): +- """ +- Get the hypervisor type of the guest vm. +- Args: +- version (Str): API version. +- host_entity (Dict): Host info dict. +- vm_entity (Dict): Vm info dict. +- Returns: +- hypervisor_type (str): Vm hypervisor type. +- """ +- hypervisor_type = None +- if version == 'v2.0': +- if host_entity: +- hypervisor_type = host_entity['hypervisor_type'] +- else: +- self._logger.warning("Cannot retrieve the host type. Version:%s" +- % version) +- else: +- if vm_entity: +- if 'resources' in vm_entity: +- if 'hypervisor_type' in vm_entity['resources']: +- hypervisor_type = vm_entity['resources']['hypervisor_type'] +- else: +- self._logger.debug("Hypervisor type of the %s is not available" +- % vm_entity['uuid']) +- else: +- self._logger.warning("No vm entity is provided for version %s. " +- "Therefore it's unable to retrieve host type" +- % version) +- return hypervisor_type +- +- def get_common_ver_url_and_method(self, cmd_key): +- """ +- Gets the correct cmd name based on its corresponding version. +- Args: +- cmd_key (str): Key name to search for in the command dict. +- Returns: +- (str, str) : Tuple of (command, rest_type). +- """ +- return (ahv_constants.CMN_RST_CMD[cmd_key]['url'], +- ahv_constants.CMN_RST_CMD[cmd_key]['method']) +- +- def get_diff_ver_url_and_method(self, cmd_key, intf_version): +- """ +- Gets the correct cmd name based on its corresponding version +- Args: +- cmd_key (str): Key name to search for in the command dict. +- intf_version (str): Interface version. +- Returns: +- (str, str) : Tuple of (command, rest_type). +- """ +- return (ahv_constants.REST_CMD[intf_version][cmd_key]['url'], +- ahv_constants.REST_CMD[intf_version][cmd_key]['method']) +- +- def get(self, uri, *args, **kwargs): +- """ +- Args are appended to the url as components. +- /arg1/arg2/arg3 +- Send a get request with kwargs to the server. +- Args: +- uri (str): Uri. +- args (list): Args. +- kwargs (dict): Dictionary of params. +- Returns: +- Response (requests.Response): rsp. +- """ +- url = self._make_url(uri, *args) +- return self._send('get', url, **kwargs) +- +- def post(self, uri, **kwargs): +- """ +- Send a Post request to the server. +- Body can be either the dict or passed as kwargs +- headers is a dict. +- Args: +- uri (str): Uri. +- kwargs (dict): Dictionary of params. +- Returns: +- Response (requests.Response): rsp. +- """ +- url = self._make_url(uri) +- return self._send('post', url, **kwargs) +- +- def make_rest_call(self, method, uri, *args, **kwargs): +- """This method calls the appropriate rest method based on the arguments. +- +- Args: +- method (str): HTTP method. +- uri (str): Relative_uri. +- args(any): Arguments. +- kwargs(dict): Key value pair for the additional args. +- +- Returns: +- rsp (dict): The response content loaded as a JSON. +- """ +- func = getattr(self, method) +- return func(uri, *args, **kwargs) +- +- def _send(self, method, url, **kwargs): +- """This private method acting as proxy for all http methods. +- Args: +- method (str): The http method type. +- url (str): The URL to for the Request +- kwargs (dict): Keyword args to be passed to the requests call. +- retries (int): The retry count in case of HTTP errors. +- Except the codes in the list NO_RETRY_HTTP_CODES. +- +- Returns: +- Response (requests.Response): The response object. +- """ +- kwargs['verify'] = kwargs.get('verify', False) +- if 'timeout' not in kwargs: +- kwargs['timeout'] = self._timeout +- if 'data' not in kwargs: +- body = {} +- kwargs['data'] = json.dumps(body) +- content_dict = {'content-type': 'application/json'} +- kwargs.setdefault('headers', {}) +- kwargs['headers'].update(content_dict) +- +- func = getattr(self._session, method) +- response = None +- +- retries = kwargs.pop("retries", None) +- retry_interval = kwargs.pop("retry_interval", self._retry_interval) +- retry_count = retries if retries else self._retries +- for ii in range(retry_count): +- try: +- response = func(url, **kwargs) +- if self._internal_debug: +- self._logger.debug("%s method The request url sent: %s" % ( +- method.upper(), response.request.url)) +- self._logger.debug('Response status: %d' % response.status_code) +- self._logger.debug('Response: %s' % json.dumps(response.json(), +- indent=4)) +- +- except (ConnectionError, ReadTimeout) as e: +- self._logger.warning("Request failed with error: %s" % e) +- if ii != retry_count - 1: +- time.sleep(retry_interval) +- continue +- finally: +- self._session.close() +- if response.ok: +- return response +- if response.status_code in [401, 403]: +- raise virt.VirtError('HTTP Auth Failed %s %s. \n res: response: %s' % +- (method, url, response)) +- elif response.status_code == 409: +- raise virt.VirtError('HTTP conflict with the current state of the ' +- 'target resource %s %s. \n res: %s' % +- (method, url, response)) +- elif response.status_code in self.NO_RETRY_HTTP_CODES: +- break +- if ii != retry_count - 1: +- time.sleep(retry_interval) +- +- if response is not None: +- msg = 'HTTP %s %s failed: ' % (method, url) +- if hasattr(response, "text") and response.text: +- msg = "\n".join([msg, response.text]).encode('utf-8') +- self._logger.error(msg) +- else: +- self._logger.error("Failed to make the HTTP request (%s, %s)" % +- (method, url)) +- +- def get_tasks(self, timestamp, version, is_pc=False): +- """ +- Returns a list of AHV tasks which happened after timestamp. +- Args: +- timestamp (int): Current timestamp. +- version (str): Interface version. +- is_pc (bool): Flag to determine f we need to poll for PC tasks. +- Returns: +- Task list (list): list of tasks. +- """ +- ahv_clusters = self.get_ahv_cluster_uuids(version) +- (uri, cmd_method) = self.get_common_ver_url_and_method(cmd_key='get_tasks') +- # For task return. Use fv2.0 for now. update the url to use v2.0. +- url = self._url[:(self._url).rfind('v')] + 'v2.0' + uri +- +- res = self._send(method=cmd_method, url=url) +- data = res.json() +- +- if is_pc: +- return self.get_pc_tasks(data, timestamp, ahv_clusters) +- else: +- return self.get_pe_tasks(data, timestamp, ahv_clusters) +- +- def get_pc_tasks(self, data, timestamp, ahv_clusters): +- """ +- Returns a list of AHV tasks on PC which happened after timestamp. +- Args: +- data (json): Rest response in json format. +- timestamp (str): Current timestamp. +- ahv_clusters (list): List of ahv clusters uuid. +- Returns: +- task_list (list): list of tasks on PC. +- """ +- (uri, cmd_method) = self.get_common_ver_url_and_method(cmd_key='get_task') +- # For task return. Use fv2.0 for now. update the url to use v2.0. +- url = self._url[:(self._url).rfind('v')] + 'v2.0' + uri +- +- task_completed = False +- task_list = [] +- if 'entities' in data: +- for task in data['entities']: +- if 'start_time_usecs' in task: +- if task['start_time_usecs'] > timestamp: +- +- if 'progress_status' in task: +- if task['progress_status'] in ahv_constants.TASK_COMPLETE_MSG: +- task_completed = True +- elif 'status' in task: +- if task['status'] in ahv_constants.TASK_COMPLETE_MSG: +- task_completed = True +- +- if task_completed: +- task_completed=False +- if 'subtask_uuid_list' in task: +- for subtask in task['subtask_uuid_list']: +- url = url % subtask +- subtask_resp = self._send(cmd_method, url) +- subtask_data = subtask_resp.json() +- +- if 'progress_status' in subtask_data: +- if subtask_data['progress_status'] in \ +- ahv_constants.TASK_COMPLETE_MSG: +- +- if 'cluster_uuid' in subtask_data: +- cluster_uuid = subtask_data['cluster_uuid'] +- else: +- # Task does not have any cluster associated with it, +- # skip it. +- continue +- +- if cluster_uuid in ahv_clusters: +- if 'entity_list' in task: +- entity_type_list = task['entity_list'] +- else: +- # Task doesn't have any entity list, skip it. +- continue +- +- if entity_type_list: +- for ent_type in entity_type_list: +- if 'entity_type' in ent_type: +- if (str(ent_type['entity_type'])).lower() \ +- in self.event_types: +- task_list.append(task) +- task_list.append(subtask_data) +- +- else: +- # Task has not finished or it failed, skip it and continue +- # the loop +- continue +- +- return task_list +- +- +- +- def get_pe_tasks(self, data, timestamp, ahv_clusters): +- """ +- Returns a list of AHV tasks on PE which happened after timestamp. +- Args: +- data (json): rest response in json format. +- timestamp (str): Current timestamp. +- ahv_clusters (list): list of ahv clusters uuid. +- Returns: +- task_list (list): list of tasks on PE. +- """ +- task_completed = False +- task_list = [] +- +- if 'entities' in data: +- for task in data['entities']: +- if 'start_time_usecs' in task: +- if task['start_time_usecs'] > timestamp: +- +- if 'progress_status' in task: +- if task['progress_status'] in ahv_constants.TASK_COMPLETE_MSG: +- task_completed = True +- elif 'status' in task: +- if task['status'] in ahv_constants.TASK_COMPLETE_MSG: +- task_completed = True +- +- if task_completed: +- task_completed = False +- if 'cluster_reference' in task: +- if 'uuid' in task['cluster_reference']: +- cluster_uuid = task['cluster_reference']['uuid'] +- elif 'cluster_uuid' in task: +- cluster_uuid = task['cluster_uuid'] +- else: +- # Task does not have any cluster associated with it, skip it. +- continue +- +- if cluster_uuid in ahv_clusters: +- if 'entity_list' in task: +- entity_type_list = task['entity_list'] +- elif 'entity_reference_list' in task: +- entity_type_list = task['entity_reference_list'] +- else: +- # Task doesn't have any entity list, skip it. +- continue +- +- for ent_type in entity_type_list: +- if 'entity_type' in ent_type: +- if (str(ent_type['entity_type'])).lower() \ +- in self.event_types: +- task_list.append(task) +- elif 'kind' in ent_type: +- if (str(ent_type['kind'])).lower() in self.event_types: +- task_list.append(task) +- else: +- # Task doesn't have any event type associated to it. +- continue +- return task_list +- +- def get_vms_uuid(self, version): +- """ +- Returns the list of vms uuid. +- Args: +- version (str): Interface version. +- Returns: +- vm_uuid_list (list): list of vm's uuid. +- """ +- self._logger.info("Getting the list of available vms") +- is_pc=True if version == 'v3' else False +- vm_uuid_list = [] +- length = 0 +- offset = 0 +- total_matches = 0 +- count = 1 +- current = 0 +- (url, cmd_method) = self.get_diff_ver_url_and_method( +- cmd_key='list_vms', intf_version=version) +- res = self.make_rest_call(method=cmd_method, uri=url) +- data = res.json() +- if 'metadata' in data: +- if 'total_matches' in data['metadata'] and 'length' in data['metadata']: +- length = data['metadata']['length'] +- total_matches = data['metadata']['total_matches'] +- elif 'count' in data['metadata'] and \ +- 'grand_total_entities' in data['metadata'] and \ +- 'total_entities' in data['metadata']: +- +- total_matches = data['metadata']['grand_total_entities'] +- count = data['metadata']['count'] +- length = data['metadata']['total_entities'] +- +- if length < total_matches: +- self._logger.debug('Number of vms %s returned from REST is less than the total'\ +- 'numberr:%s. Adjusting the offset and iterating over all'\ +- 'vms until evry vm is returned from the server.' % (length, +- total_matches)) +- count = math.ceil(total_matches/float(length)) +- +- body = {'length': length, 'offset': offset} +- for i in self._progressbar(range(int(count)), "Finding vms uuid: ", total=int(total_matches), is_pc=is_pc): +- if 'entities' in data: +- for vm_entity in data['entities']: +- if 'metadata' in vm_entity: +- vm_uuid_list.append(vm_entity['metadata']['uuid']) +- elif 'uuid' in vm_entity: +- vm_uuid_list.append(vm_entity['uuid']) +- else: +- self._logger.warning("Cannot access the uuid for the vm %s. " +- "vm object: %s" % (vm_entity['name'], +- vm_entity)) +- +- body['offset'] = body['offset'] + length +- body_data = json.dumps(body, indent=4) +- self._logger.debug('next vm list call has this body: %s' % body) +- res = self.make_rest_call(method=cmd_method, uri=url, data=body_data) +- data = res.json() +- current += 1 +- +- self._logger.info("Total number of vms uuids found and saved for processing %s" % len(vm_uuid_list)) +- return vm_uuid_list +- +- def get_hosts_uuid(self, version): +- """ +- Returns the list of host uuid. +- Args: +- version (str): Interface version. +- Returns: +- host_uuid_list (list): list of host's uuid. +- """ +- host_uuid_list = [] +- (url, cmd_method) = self.get_diff_ver_url_and_method( +- cmd_key='list_hosts', intf_version=version) +- +- res = self.make_rest_call(method=cmd_method, uri=url) +- data = res.json() +- if 'entities' in data: +- for host_entity in data['entities']: +- if 'status' in host_entity and'metadata' in host_entity: +- # Check if a physical host, not a cluster. +- if 'cpu_model' in host_entity['status']: +- host_uuid_list.append(host_entity['metadata']['uuid']) +- elif 'uuid' in host_entity: +- host_uuid_list.append(host_uuid_list['uuid']) +- else: +- self._logger.warning("Cannot access the uuid for the. " +- "host object: %s" % (host_entity)) +- +- +- def get_host_cluster_uuid(self, host_info): +- """ +- Returns host's cluster UUID. +- Args: +- host_info (dict): Host info dict. +- Returns: +- host_cluster_uuid (uuid): host's cluster uuid. +- """ +- if 'cluster_uuid' in host_info: +- return host_info['cluster_uuid'] +- elif 'cluster_reference' in host_info: +- return host_info['cluster_reference']['uuid'] +- +- def get_ahv_cluster_uuids(self, version): +- """ +- Returns list of ahv cluster uuids. +- Args: +- version (str): Interface version. +- Returns: +- ahv_host_cluster_uuids (List): Returns list of ahv cluster uuids. +- """ +- ahv_host_cluster_uuids = [] +- seen = set(ahv_host_cluster_uuids) +- +- (url, cmd_method) = self.get_diff_ver_url_and_method( +- cmd_key='list_clusters', intf_version=version) +- res = self.make_rest_call(method=cmd_method, uri=url) +- data = res.json() +- +- formatted_data = self._format_response(data) +- +- for cluster in formatted_data: +- if 'hypervisor_types' in cluster and 'cluster_uuid' in cluster: +- for hypevirsor_type in cluster['hypervisor_types']: +- if hypevirsor_type in ahv_constants.AHV_HYPERVIRSOR: +- cluster_uuid = cluster['cluster_uuid'] +- if cluster_uuid not in seen: +- seen.add(cluster_uuid) +- ahv_host_cluster_uuids.append(cluster['cluster_uuid']) +- break +- +- return ahv_host_cluster_uuids +- +- def get_host_version(self, host_info): +- """ +- Returns host's version. +- Args: +- host_info (dict): Host info dict. +- Returns: +- host_version (Str): Host version if found, None otherwise. +- """ +- host_version = None +- if 'resources' in host_info: +- host_resources = host_info['resources'] +- if 'hypervisor' in host_resources: +- if 'hypervisor_full_name' in host_resources['hypervisor']: +- host_version = host_resources['hypervisor']['hypervisor_full_name'] +- elif 'hypervisor_full_name' in host_info: +- host_version = host_info['hypervisor_full_name'] +- else: +- self._logger.warning("Cannot get host version for %s" +- % host_info['uuid']) +- +- return host_version +- +- def get_vm(self, uuid): +- """ +- Returns vm information +- Args: +- uuid (str): Vm uuid. +- Return: +- data (dict): Vm information. +- """ +- (url, cmd_method) = self.get_common_ver_url_and_method(cmd_key='get_vm') +- url = url % uuid +- res = self.make_rest_call(method=cmd_method, uri=url) +- if res: +- data = res.json() +- return self._format_response(data) +- return None +- +- def get_host(self, uuid): +- """ +- Returns host information +- Args: +- uuid (str): Host uuid. +- Return: +- data (dict): Host information. +- """ +- (url, cmd_method) = self.get_common_ver_url_and_method(cmd_key='get_host') +- url = url % uuid +- res = self.make_rest_call(method=cmd_method, uri=url) +- if res: +- data = res.json() +- return self._format_response(data) +- else: +- return None +- +- def get_vm_host_uuid_from_vm(self, vm_entity): +- """ +- Get the host uuid from the vm_entity response +- Args: +- vm_entity (dict): Vm info. +- Returns: +- host uuid (str): Vm host uuid if found, none otherwise. +- """ +- if 'resources' in vm_entity: +- if 'host_reference' in vm_entity['resources']: +- return vm_entity['resources']['host_reference']['uuid'] +- else: +- self._logger.warning("Did not find any host information for vm:%s" +- % vm_entity['uuid']) +- elif 'host_uuid' in vm_entity: +- return vm_entity['host_uuid'] +- else: +- # Vm is off therefore no host is assigned to it. +- self._logger.debug('Cannot get the host uuid of the vm:%s. ' +- 'perhaps the vm is powered off' % vm_entity['uuid']) +- return None +- +- def is_ahv_host(self, version, host_uuid, vm_entity=None): +- """ +- Determine if a given host is a AHV host. +- host uuid should match the host uuid in vm_entity. +- Args: +- version (str): API version. +- host_uuid (str): uuid of a host. +- vm_entity (dict): For v3 +- Returns: +- bool : True if host is ahv; false otehrwise. +- """ +- if version == 'v2.0': +- host = self.get_host(host_uuid) +- if 'hypervisor_type' in host: +- return host['hypervisor_type'] in ahv_constants.AHV_HYPERVIRSOR +- else: +- if 'resources' in vm_entity: +- if 'hypervisor_type' in vm_entity['resources']: +- return vm_entity['resources']['hypervisor_type'] in \ +- ahv_constants.AHV_HYPERVIRSOR +- self._logger.debug('Hypervisor type not found. \nversion:%s, ' +- '\nhost_uuid:%s, \nvm_entity:%s' +- % (version, host_uuid, vm_entity)) +- return False +- +- def build_host_to_uvm_map(self, version): +- """ +- Builds a dictionary of every ahv host along with the vms they are hosting +- Args: +- version (Str): API version +- Returns: +- host_uvm_map (dict): Dict of ahv host with its uvms. +- """ +- host_uvm_map = {} +- vm_entity = None +- host_uuid = None +- vm_uuids = self.get_vms_uuid(version) +- +- self._logger.info("Processing hosts for each vm.") +- if len(vm_uuids) > 0: +- for vm_uuid in vm_uuids: +- vm_entity = self.get_vm(vm_uuid) +- if vm_entity: +- host_uuid = self.get_vm_host_uuid_from_vm(vm_entity) +- if host_uuid: +- if self.is_ahv_host(version, host_uuid, vm_entity): +- host = self.get_host(host_uuid) +- if host: +- if host_uuid not in host_uvm_map: +- host_uvm_map[host_uuid] = host +- if 'guest_list' in host_uvm_map[host_uuid]: +- host_uvm_map[host_uuid]['guest_list'].append(vm_entity) +- else: +- host_uvm_map[host_uuid]['guest_list'] = [] +- host_uvm_map[host_uuid]['guest_list'].append(vm_entity) +- else: +- self._logger.warning("unable to read information for host %s" % host_uuid) +- continue +- else: +- self._logger.debug("Host %s is not ahv, skipping it." % host_uuid) +- continue +- host_type = self.get_hypervisor_type(version, host, vm_entity) +- host_uvm_map[host_uuid]['hypervisor_type'] = host_type +- else: +- self._logger.warning("No available vms found") +- try: +- host_uuids = self.get_hosts_uuid(version) +- if len(host_uuids) > 0: +- for host_uuid in host_uuids: +- host = self.get_host(host_uuid) +- if host_uuid not in host_uvm_map: +- host_uvm_map[host_uuid] = host +- host_uvm_map[host_uuid]['guest_list'] = [] +- +- else: +- self._logger.warning("No Available AHV host found") +- except TypeError: +- # In case there is no cluster registered to the PC. +- self._logger.warning("Unable to find any AHV hosts.") +- +- return host_uvm_map +- +-class Failure(Exception): +- +- def __init__(self, details): +- self.details = details +- +- def __str__(self): +- try: +- return str(self.details) +- except Exception as exn: +- import sys +- print(exn) +- return "AHV-API failure: %s" % str(self.details) +- +- def _details_map(self): +- return dict([(str(i), self.details[i]) for i in range(len(self.details))]) +diff --git a/virtwho/virt/virt.py b/virtwho/virt/virt.py +index 5495657..466dc90 100644 +--- a/virtwho/virt/virt.py ++++ b/virtwho/virt/virt.py +@@ -923,7 +923,6 @@ class Virt(IntervalThread): + import virtwho.virt.hyperv # flake8: noqa + import virtwho.virt.fakevirt # flake8: noqa + import virtwho.virt.kubevirt # flake8: noqa +- import virtwho.virt.ahv # flake8: noqa + + return [subcls for subcls in cls.__subclasses__()] + +-- +2.25.3 + diff --git a/sources b/sources index 7c2148c..4280b16 100644 --- a/sources +++ b/sources @@ -1 +1,2 @@ -SHA512 (virt-who-0.29.0.tar.gz) = 727b33aa443f55a5a9a2de430660dbc56684526e5bec1c4ddf890849cdd527d4c387b89bbb5073e9099ae4fb241308c8fc756b92193f1601f20371287cb58db7 +SHA512 (virt-who-0.31.0.tar.gz) = f9d0247d333fb2544c6f54b12cc56ffb455224ccf1cd8e53ab1caa7272916ab9db316e4ed8bc13adc484e7c7f8797da32b526b4735e67fe80f20353b99e53f08 +SHA512 (build-rpm-no-ahv.patch) = 0eafc41c1b16ff5823cc8c74625c80d4581fbc802fa802cf279d7b1828aab97028124ac26501eb70b258d44c3b6fc6f5644db1f193208f8411f6c733210748e1 diff --git a/virt-who.spec b/virt-who.spec index ae70b4d..627ef93 100644 --- a/virt-who.spec +++ b/virt-who.spec @@ -20,8 +20,8 @@ Name: virt-who -Version: 0.29.0 -Release: %{release_number}%{?dist}.2 +Version: 0.31.0 +Release: %{release_number}%{?dist} Summary: Agent for reporting virtual guest IDs to subscription-manager @@ -29,6 +29,7 @@ Group: System Environment/Base License: GPLv2+ URL: https://github.com/candlepin/virt-who Source0: %{name}-%{version}.tar.gz +Patch1: build-rpm-no-ahv.patch BuildArch: noarch BuildRequires: %{python_ver}-devel @@ -91,7 +92,9 @@ report them to the subscription manager. %prep %setup -q - +%if 0%{?rhel} +%patch1 -p1 +%endif %build %{python_exec} setup.py build --rpm-version=%{version}-%{release_number} @@ -168,11 +171,39 @@ fi %changelog -* Wed Jul 29 2020 Fedora Release Engineering - 0.29.0-1.2 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild +* Thu Oct 22 2020 William Poteat 0.31.0-1 +- 1876927: virt-who fails to parse output from hypervisor (wpoteat@redhat.com) +- Additional copy of patch file needed for build Update of Fedora versions in + releaser file (wpoteat@redhat.com) +- Correction in patch builder for directory location (wpoteat@redhat.com) +- Update releasers (wpoteat@redhat.com) +- 1878136: Deprecation comment in config file (wpoteat@redhat.com) +- 1854829: rhsm_port and rhsm_password are missing in template.conf + (wpoteat@redhat.com) + +* Thu Oct 01 2020 William Poteat 0.30.0-1 +- Add patch to remove AHV bits for RHEL builds (wpoteat@redhat.com) +- 1878136: Deprecation warning for environment variables (wpoteat@redhat.com) +- 184506: virt-who should send its version in the User-Agent header + (wpoteat@redhat.com) +- 1806572: RHEVM API url needs version specified (wpoteat@redhat.com) +- 1847792: [ESX] Virt-who is failed when run with + "filter/exclude_host_parents=" option (wpoteat@redhat.com) +- 1809098: Convert UUID to big-endian for certain esx hardware versions + (wpoteat@redhat.com) +- 1835132: support milicpus (piotr.kliczewski@gmail.com) + +* Thu May 21 2020 William Poteat 0.29.2-1 +- NTLM: Fix compatibility issue with Python3.8 (jhnidek@redhat.com) +- 1806572: RHEVM should only use version 4 (wpoteat@redhat.com) +- Update to tests to match changes in Subscription Manager (jhnidek@redhat.com) +- 1461272: Filter virt-who hosts based on host_parents using wildcard + (wpoteat@redhat.com) -* Tue May 26 2020 Miro HronĨok - 0.29.0-1.1 -- Rebuilt for Python 3.9 +* Fri May 08 2020 William Poteat 0.29.1-1 +- 1806572: virt-who using V3 APIs for communication with RHEVM which is + deprecated (wpoteat@redhat.com) +- Update Fedora releases (wpoteat@redhat.com) * Fri Apr 03 2020 William Poteat 0.29.0-1 - Update releasers for RHEL-8.3 (wpoteat@redhat.com)