diff --git a/swatch-tally/ct/java/tests/BaseTallyComponentTest.java b/swatch-tally/ct/java/tests/BaseTallyComponentTest.java index d2ad26fc9f..8c308d7ae4 100644 --- a/swatch-tally/ct/java/tests/BaseTallyComponentTest.java +++ b/swatch-tally/ct/java/tests/BaseTallyComponentTest.java @@ -97,6 +97,7 @@ public class BaseTallyComponentTest { // --- Instance fields --- protected final TallyDbHostSeeder seeder = new TallyDbHostSeeder(swatchDatabase); + protected String orgId; protected RbacAccessTestHelper rbacHelper; diff --git a/swatch-tally/ct/java/tests/TallyNightlyHbiTest.java b/swatch-tally/ct/java/tests/TallyNightlyHbiTest.java index 9beb58f170..8ddcec5bc4 100644 --- a/swatch-tally/ct/java/tests/TallyNightlyHbiTest.java +++ b/swatch-tally/ct/java/tests/TallyNightlyHbiTest.java @@ -20,22 +20,23 @@ */ package tests; -import static com.redhat.swatch.component.tests.utils.Topics.TALLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static utils.TallyTestProducts.RHEL_FOR_X86; -import api.MessageValidators; -import com.redhat.swatch.tally.test.model.TallySnapshot.Granularity; +import com.redhat.swatch.component.tests.api.hbi.HbiDbConnector; +import com.redhat.swatch.component.tests.api.hbi.HostConnector.SeededHost; +import com.redhat.swatch.component.tests.api.hbi.HostStateManager; +import com.redhat.swatch.tally.test.model.ServiceLevelType; import java.time.OffsetDateTime; +import java.util.List; import java.util.Map; +import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import utils.TallyHbiDbSeeder; -import utils.TallyHbiDbSeeder.SeededHost; /** * Component tests for nightly tally with HBI database integration. @@ -44,27 +45,24 @@ * *
    *
  1. Insert host into HBI database - *
  2. Send HBI Kafka event (simulating swatch-metrics-hbi) - *
  3. Verify swatch-tally syncs the host - *
  4. Run nightly tally + *
  5. Run nightly tally (reads from HBI) *
  6. Verify tally results *
*/ public class TallyNightlyHbiTest extends BaseTallyComponentTest { - private TallyHbiDbSeeder hbiSeeder; + private HostStateManager hostManager; + private static final String RHEL_PRODUCT_ID = "69"; @BeforeEach - void setupHbiSeeder() { - // Initialize HBI seeder with database service (auto-configured for local/OpenShift) - hbiSeeder = new TallyHbiDbSeeder(hbiDatabase); + void setupHostManager() { + hostManager = new HostStateManager(new HbiDbConnector(hbiDatabase)); } @AfterEach - void cleanupHbiHosts() { - // Rollback: delete all HBI hosts inserted during test - if (hbiSeeder != null) { - hbiSeeder.deleteAllInsertedHosts(); + void cleanupHosts() { + if (hostManager != null) { + hostManager.cleanupAll(); } } @@ -81,19 +79,175 @@ void testHbiSeederCanInsert() { // Given: No specific setup needed beyond @BeforeEach // When: Inserting a RHEL host into HBI database - SeededHost host = hbiSeeder.insertRhelHost(orgId); + SeededHost host = hostManager.createRhsmHost(orgId).physicalRhel1Socket0Cores().insert(); // Then: Host is tracked with expected metadata assertNotNull(host.hostId(), "Host ID should be generated"); - assertTrue( - host.inventoryId().startsWith("test-inventory-id"), - "Inventory ID should have expected prefix"); - assertTrue( - host.subscriptionManagerId().startsWith("test-subman-id"), - "Subscription manager ID should have expected prefix"); assertEquals(orgId, host.orgId(), "Org ID should match"); - assertEquals(1, hbiSeeder.getInsertedHostCount(), "Seeder should track one host"); - assertTrue(hbiSeeder.hostExists(host.hostId()), "Host should exist in HBI database"); + assertEquals(1, hostManager.getTrackedCount(), "Seeder should track one host"); + assertTrue(hostManager.hostExists(host.hostId()), "Host should exist in HBI database"); + } + + /** + * Demonstrates both approaches to creating HBI hosts. + * + *

**Preset Template Approach** (host1): Uses `.physicalRhel1Socket0Cores()` which applies all + * RHEL physical defaults automatically (infrastructure, arch, RHEL product facts). + * + *

**Manual Builder Approach** (host2): Sets each property explicitly using the fluent builder + * pattern. Useful when you need fine-grained control or custom configurations. + * + *

Both approaches produce valid hosts tracked by the HostStateManager. + */ + @Test + void testCanInsertMultipleHosts() { + // Given: No specific setup needed beyond @BeforeEach + + // When: Inserting multiple hosts into HBI database + SeededHost host1 = hostManager.createRhsmHost(orgId).physicalRhel1Socket0Cores().insert(); + + SeededHost host2 = + hostManager + .createRhsmHost(orgId) + .infrastructureType("physical") + .arch("x86_64") + .rhsmFact("IS_VIRTUAL", "false") + .rhsmFact("RH_PROD", List.of(RHEL_PRODUCT_ID)) + .rhsmFact("ARCHITECTURE", "x86_64") + .sockets(1) + .cores(0) + .displayName("Test Host - 1") + .insert(); + + // Then: Hosts are tracked with expected metadata + assertNotNull(host1.hostId(), "Host ID should be generated"); + assertNotNull(host2.hostId(), "Host ID should be generated"); + } + + /** + * - **Description**: Verify that we can insert a host in the HBI database with multiple + * reporters- **Setup**: Component test environment with swatch-tally is running and an instance + * of insights db is up - **Action**: Insert a host into the HBI database with multiple + * conflicting reporters, satellite facts and rhsm facts ( rhsm facts will overwrite satellite + * facts ) - - **Verification**: - a host was returned from the insert - the host returned has a + * inventory id - the host returned has an id - the host returned has the expected orgId - + * **Expected Result**: The host was inserted into the HBI database + */ + @Test + void testCanCreateHostWithMultipleReporters() { + // Given: The org has opted in + service.createOptInConfig(orgId); + + // When: Inserting multiple hosts into HBI database + SeededHost host = + hostManager + .createSatelliteHost(orgId) + // Satellite Facts replicated from prod data + .satelliteFact("satellite_version", "6.17.6.2") + .satelliteFact("virtual_host_name", "virt-name-for-testing.com") + .satelliteFact("virtual_host_uuid", UUID.randomUUID().toString()) + .satelliteFact("system_purpose_sla", "Standard") + .satelliteFact("system_purpose_role", "Red Hat Enterprise Linux Server") + .satelliteFact("system_purpose_usage", "Component Testing") + .addRhsmReporter() + // RHEL Facts replicated from prod data + .infrastructureType("physical") + .cloudProvider("aws") + .arch("x86_64") + .providerId(UUID.randomUUID().toString()) + .rhsmFact("IS_VIRTUAL", "false") + .rhsmFact("RH_PROD", List.of("69")) + .rhsmFact("ARCHITECTURE", "x86_64") + .rhsmFact("Memory", 63) + // Set to envoke the nomalization logic that will overwrite the normalize from satellite + // facts with the + .rhsmFact("SYSPURPOSE_SLA", "Premium") + .rhsmFact("SYSPURPOSE_ROLE", "Red Hat Enterprise Linux Server") + .rhsmFact("SYSPURPOSE_USAGE", "Component Testing") + .sockets(1) + .cores(8) + .arch("x86_64") + .insert(); + + assertNotNull(host.hostId(), "Host ID should be generated"); + + // When: Nightly tally runs + service.tallyOrg(orgId); + + // Then: The instance appears in the instances API + OffsetDateTime beginning = OffsetDateTime.now().minusDays(1); + OffsetDateTime ending = OffsetDateTime.now().plusDays(1); + + var instanceResponse = + service.getInstancesByProduct(orgId, RHEL_FOR_X86.productTag(), beginning, ending); + + assertNotNull(instanceResponse.getData(), "Instance response should have data"); + assertFalse(instanceResponse.getData().isEmpty(), "Instance response should not be empty"); + + assertThatExpectedReportPopulated(orgId, RHEL_FOR_X86.productTag(), beginning, ending, true); + } + + /** + * - **Description**: Verify that we can insert a host in the HBI database with multiple + * reporters- **Setup**: Component test environment with swatch-tally is running and an instance + * of insights db is up - **Action**: Insert a host into the HBI database with multiple + * conflicting reporters, satellite facts and rhsm facts ( rhsm facts will WILL NOT overwrite the + * satellite facts ) - - **Verification**: - a host was returned from the insert - the host + * returned has a inventory id - the host returned has a an id - the host returned has the + * expected orgId - **Expected Result**: The host was inserted into the HBI database + */ + @Test + void testCanCreateHostWithMultipleReportersOverrideRhsm() { + // Given: The org has opted in + service.createOptInConfig(orgId); + + // When: Inserting multiple hosts into HBI database + SeededHost host = + hostManager + .createSatelliteHost(orgId) + // Satellite Facts replicated from prod data + .satelliteFact("satellite_version", "6.17.6.2") + .satelliteFact("virtual_host_name", "virt-name-for-testing.com") + .satelliteFact("virtual_host_uuid", UUID.randomUUID().toString()) + .satelliteFact("system_purpose_sla", "Standard") + .satelliteFact("system_purpose_role", "Red Hat Enterprise Linux Server") + .satelliteFact("system_purpose_usage", "Component Testing") + .addRhsmReporter() + // RHEL Facts replicated from prod data + .infrastructureType("physical") + .cloudProvider("aws") + .arch("x86_64") + .providerId(UUID.randomUUID().toString()) + .rhsmFact("IS_VIRTUAL", "false") + .rhsmFact("RH_PROD", List.of("69")) + .rhsmFact("ARCHITECTURE", "x86_64") + .rhsmFact("Memory", 63) + // Set to envoke the skiprshm logic that will use the satellite facts with the + .rhsmFact("SYNC_TIMESTAMP", "2026-08-24T00:32:05.179356374Z") + .rhsmFact("SYSPURPOSE_SLA", "Premium") + .rhsmFact("SYSPURPOSE_ROLE", "Red Hat Enterprise Linux Server") + .rhsmFact("SYSPURPOSE_USAGE", "Component Testing") + .sockets(1) + .cores(8) + .arch("x86_64") + .insert(); + + assertNotNull(host.hostId(), "Host ID should be generated"); + + // When: Nightly tally runs + service.tallyOrg(orgId); + + // Then: The instance appears in the instances API + OffsetDateTime beginning = OffsetDateTime.now().minusDays(1); + OffsetDateTime ending = OffsetDateTime.now().plusDays(1); + + var instanceResponse = + service.getInstancesByProduct(orgId, RHEL_FOR_X86.productTag(), beginning, ending); + + assertNotNull(instanceResponse.getData(), "Instance response should have data"); + assertFalse(instanceResponse.getData().isEmpty(), "Instance response should not be empty"); + + assertThatExpectedReportPopulated(orgId, RHEL_FOR_X86.productTag(), beginning, ending, false); } /** @@ -106,15 +260,15 @@ void testHbiSeederCanInsert() { @Test void testHbiSeederCanDelete() { // Given: A host is inserted into HBI database - SeededHost host = hbiSeeder.insertRhelHost(orgId); + SeededHost host = hostManager.createRhsmHost(orgId).physicalRhel1Socket0Cores().insert(); assertNotNull(host.hostId(), "Host ID should be generated"); // When: Deleting the host - hbiSeeder.deleteHost(host.hostId()); + hostManager.cleanup(host.hostId()); // Then: Host is removed from tracking and database - assertEquals(0, hbiSeeder.getInsertedHostCount(), "Seeder should track zero hosts"); - assertFalse(hbiSeeder.hostExists(host.hostId()), "Host should not exist in HBI database"); + assertEquals(0, hostManager.getTrackedCount(), "Seeder should track zero hosts"); + assertFalse(hostManager.hostExists(host.hostId()), "Host should not exist in HBI database"); } /** @@ -127,44 +281,65 @@ void testHbiSeederCanDelete() { @Test void testHbiSeederRollbackDeletesAllHosts() { // Given: Multiple hosts are inserted (mix of RHEL and cloud) - SeededHost host1 = hbiSeeder.insertRhelHost(orgId); - SeededHost host2 = hbiSeeder.insertCloudHost(orgId); - assertEquals(2, hbiSeeder.getInsertedHostCount(), "Seeder should track two hosts"); - assertTrue(hbiSeeder.hostExists(host1.hostId()), "Host 1 should exist in HBI database"); - assertTrue(hbiSeeder.hostExists(host2.hostId()), "Host 2 should exist in HBI database"); + SeededHost host1 = hostManager.createRhsmHost(orgId).insert(); + SeededHost host2 = hostManager.createRhsmHost(orgId).insert(); + assertEquals(2, hostManager.getTrackedCount(), "Seeder should track two hosts"); + assertTrue(hostManager.hostExists(host1.hostId()), "Host 1 should exist in HBI database"); + assertTrue(hostManager.hostExists(host2.hostId()), "Host 2 should exist in HBI database"); // When: Rolling back all inserted hosts - hbiSeeder.deleteAllInsertedHosts(); + hostManager.cleanupAll(); // Then: All hosts are removed from tracking and database - assertEquals(0, hbiSeeder.getInsertedHostCount(), "Seeder should track zero hosts"); - assertFalse(hbiSeeder.hostExists(host1.hostId()), "Host 1 should not exist in HBI database"); - assertFalse(hbiSeeder.hostExists(host2.hostId()), "Host 2 should not exist in HBI database"); + assertEquals(0, hostManager.getTrackedCount(), "Seeder should track zero hosts"); + assertFalse(hostManager.hostExists(host1.hostId()), "Host 1 should not exist in HBI database"); + assertFalse(hostManager.hostExists(host2.hostId()), "Host 2 should not exist in HBI database"); } /** * - **Description**: Verify that we can insert a RHEL product into the the HBI database - * **Setup**: Component test environment with swatch-tally is running, an instance of insights db * - **Action**: Create a host with a product that is a RHEL product - **Verification**: - verify - * that a tally Report for the RHEL product is not null - verify that the total sockets value in - * the tally Report is greather that or equal to the 2 sockets - **Expected Result**: All the host - * inserted into the db have been deleted from the database + * that a tally Report for the RHEL product is not null - verify that the socket count increased + * by 2 - **Expected Result**: The tally socket count increased by 2 */ @Test void testNightlyTallyRhelProduct() { - // Given: Org is opted in and RHEL host exists with known capacity + // Given: Org is opted in service.createOptInConfig(orgId); - SeededHost host = hbiSeeder.rhelHost(orgId).cores(8).sockets(2).insert(); - assertNotNull(host.hostId(), "Host should be created"); - // When: Nightly tally runs - service.tallyOrg(orgId); - - // Then: Tally report contains expected socket count + // And: Define time range (today only) OffsetDateTime beginning = OffsetDateTime.now().minusDays(1); OffsetDateTime ending = OffsetDateTime.now().plusDays(1); - var reportData = + // When: Capture initial socket count + var initialReportData = + service.getTallyReportData( + orgId, + RHEL_FOR_X86.productTag(), + "Sockets", + Map.of( + "granularity", "Daily", + "beginning", beginning.toString(), + "ending", ending.toString())); + + double initialSockets = + initialReportData.getData() != null + ? initialReportData.getData().stream() + .mapToDouble(point -> point.getValue() != null ? point.getValue() : 0.0) + .sum() + : 0.0; + + // And: RHEL host with 2 sockets and 8 cores is created + SeededHost host = + hostManager.createRhsmHost(orgId).physicalRhel2Socket2Cores().cores(8).insert(); + assertNotNull(host.hostId(), "Host should be created"); + + // And: Nightly tally runs + service.tallyOrg(orgId); + + // Then: Socket count increased by 2 + var currentReportData = service.getTallyReportData( orgId, RHEL_FOR_X86.productTag(), @@ -174,40 +349,129 @@ void testNightlyTallyRhelProduct() { "beginning", beginning.toString(), "ending", ending.toString())); - assertNotNull(reportData, "Tally report should be created"); - assertNotNull(reportData.getData(), "Report should have data"); - assertFalse(reportData.getData().isEmpty(), "Report should not be empty"); + assertNotNull(currentReportData, "Tally report should be created"); + assertNotNull(currentReportData.getData(), "Report should have data"); - boolean hasExpectedSockets = - reportData.getData().stream() - .anyMatch(point -> point.getValue() != null && point.getValue() == 2.0); - assertTrue(hasExpectedSockets, "Report should contain a data point with exactly 2 sockets"); + double currentSockets = + currentReportData.getData().stream() + .mapToDouble(point -> point.getValue() != null ? point.getValue() : 0.0) + .sum(); + + assertEquals( + initialSockets + 2.0, + currentSockets, + "Socket count should increase by 2 after adding host with 2 sockets"); } /** - * - **Description**: Verify that a cloud host with RHEL product produces a TallySummary Kafka - * message - **Setup**: Component test environment with swatch-tally is running, an instance of - * insights db - **Action**: Create a cloud host with RHEL product facts and run nightly tally - - * **Verification**: - verify that the host exists in HBI - verify that a TallySummary message - * appears on the tally Kafka topic with the expected product and metric - **Expected Result**: A - * TallySummary message is produced, proving the host was accepted by the tally process + * - **Description**: Verify that a cloud host with RHEL product appears in the instance report - + * **Setup**: Component test environment with swatch-tally is running, an instance of insights db + * - **Action**: Create a cloud host with RHEL product facts and run nightly tally - + * **Verification**: - verify that the host exists in HBI - verify that the instance appears in + * the instances API - **Expected Result**: The instance is visible in the instance report */ @Test void testNightlyTallyCloudProduct() { // Given: Org is opted in and cloud host with RHEL product exists in HBI database service.createOptInConfig(orgId); - SeededHost host = hbiSeeder.rhelHost(orgId).cores(8).sockets(2).cloudProvider("aws").insert(); + SeededHost host = hostManager.createRhsmHost(orgId).awsRhelSockets1Cores1().insert(); assertNotNull(host.hostId(), "Cloud host should be created"); - assertTrue(hbiSeeder.hostExists(host.hostId()), "Cloud host should exist in HBI database"); + assertTrue(hostManager.hostExists(host.hostId()), "Cloud host should exist in HBI database"); // When: Nightly tally runs service.tallyOrg(orgId); - // Then: A TallySummary message is produced on the tally Kafka topic - kafkaBridge.waitForKafkaMessage( - TALLY, - MessageValidators.tallySummaryMatches( - orgId, RHEL_FOR_X86.productTag(), "Sockets", Granularity.DAILY), - 1); + // Then: The instance appears in the instances API + OffsetDateTime beginning = OffsetDateTime.now().minusDays(1); + OffsetDateTime ending = OffsetDateTime.now().plusDays(1); + + var instanceResponse = + service.getInstancesByProduct(orgId, RHEL_FOR_X86.productTag(), beginning, ending); + + assertNotNull(instanceResponse.getData(), "Instance response should have data"); + assertFalse(instanceResponse.getData().isEmpty(), "Instance response should not be empty"); + assertEquals( + 1, + instanceResponse.getData().size(), + "Should have exactly one instance for this org and product"); + } + + private void assertThatExpectedReportPopulated( + String orgId, + String productTag, + OffsetDateTime beginning, + OffsetDateTime ending, + Boolean isPremium) { + var premiumData = + service.getTallyReportData( + orgId, + productTag, + "Sockets", + Map.of( + "granularity", + "Daily", + "beginning", + beginning.toString(), + "ending", + ending.toString(), + "sla", + "Premium")); + + var standardData = + service.getTallyReportData( + orgId, + productTag, + "Sockets", + Map.of( + "granularity", + "Daily", + "beginning", + beginning.toString(), + "ending", + ending.toString(), + "sla", + "Standard")); + + if (isPremium) { + // Verify Premium has data (RHSM facts won) + assertNotNull(premiumData.getData(), "Premium SLA should have data"); + assertTrue( + premiumData.getData().stream().anyMatch(point -> point.getHasData()), + "Premium SLA should have at least one data point with hasData=true"); + assertEquals( + ServiceLevelType.PREMIUM, + premiumData.getMeta().getServiceLevel(), + "Meta should show Premium SLA"); + + // Verify Standard has NO data (Satellite facts were overridden) + assertNotNull(standardData.getData(), "Standard SLA should return data structure"); + assertTrue( + standardData.getData().stream().noneMatch(point -> point.getHasData()), + "Standard SLA should have NO actual data (Satellite SLA was overridden by RHSM Premium)"); + assertEquals( + ServiceLevelType.STANDARD, + standardData.getMeta().getServiceLevel(), + "Meta should show Standard SLA"); + } else { + // Verify Standard has data (Standard facts being used) + assertNotNull(standardData.getData(), "Standard SLA should have data"); + assertTrue( + standardData.getData().stream().anyMatch(point -> point.getHasData()), + "Standard SLA should have at least one data point with hasData=true"); + assertEquals( + ServiceLevelType.STANDARD, + standardData.getMeta().getServiceLevel(), + "Meta should show Standard SLA"); + + // Verify Premium has NO data + assertNotNull(premiumData.getData(), "Premium SLA should return data structure"); + assertTrue( + premiumData.getData().stream().noneMatch(point -> point.getHasData()), + "Premium SLA should have NO actual data"); + assertEquals( + ServiceLevelType.PREMIUM, + premiumData.getMeta().getServiceLevel(), + "Meta should show Premium SLA"); + } } } diff --git a/swatch-tally/ct/java/tests/TallyRhelTest.java b/swatch-tally/ct/java/tests/TallyNightlyTest.java similarity index 80% rename from swatch-tally/ct/java/tests/TallyRhelTest.java rename to swatch-tally/ct/java/tests/TallyNightlyTest.java index 0ed79700c0..b2e5c308d6 100644 --- a/swatch-tally/ct/java/tests/TallyRhelTest.java +++ b/swatch-tally/ct/java/tests/TallyNightlyTest.java @@ -28,19 +28,21 @@ import static utils.TallyTestProducts.RHEL_FOR_X86; import com.redhat.swatch.component.tests.api.TestPlanName; +import com.redhat.swatch.component.tests.api.hbi.HbiDbConnector; +import com.redhat.swatch.component.tests.api.hbi.HostConnector.SeededHost; +import com.redhat.swatch.component.tests.api.hbi.HostStateManager; import com.redhat.swatch.component.tests.logging.Log; import com.redhat.swatch.tally.test.model.InstanceData; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import utils.TallyHbiDbSeeder; -import utils.TallyHbiDbSeeder.SeededHost; /** * Component tests for RHEL physical host tally with socket increase mapping. @@ -50,30 +52,35 @@ * *

Matches IQE test: test_validate_tally_on_physical_rhel_sockets */ -public class TallyRhelTest extends BaseTallyComponentTest { +public class TallyNightlyTest extends BaseTallyComponentTest { - private TallyHbiDbSeeder hbiSeeder; + private HostStateManager hostManager; + + /** + * Socket increase mapping for RHEL physical hosts. Maps actual socket count -> reported socket + * count for tally. + */ + private static final Map RHEL_PER_SOCKET_INCREASE = + Map.of(1, 2, 2, 2, 4, 4, 7, 8); /** * Provider for socket increase mapping test parameters. Matches * IQE's @pytest.mark.parametrize("sockets", rhel_per_socket_increase.keys()) */ static Stream socketMappingProvider() { - return TallyHbiDbSeeder.getRhelPerSocketIncreaseMap().entrySet().stream() + return RHEL_PER_SOCKET_INCREASE.entrySet().stream() .map(entry -> Arguments.of(entry.getKey(), entry.getValue())); } @BeforeEach - void setupHbiSeeder() { - // Initialize HBI seeder with database service (auto-configured for local/OpenShift) - hbiSeeder = new TallyHbiDbSeeder(hbiDatabase); + void setupHostManager() { + hostManager = new HostStateManager(new HbiDbConnector(hbiDatabase)); } @AfterEach - void cleanupHbiHosts() { - // Rollback: delete all HBI hosts inserted during test - if (hbiSeeder != null) { - hbiSeeder.deleteAllInsertedHosts(); + void cleanupHosts() { + if (hostManager != null) { + hostManager.cleanupAll(); } } @@ -88,14 +95,11 @@ void cleanupHbiHosts() { * correct display_name, category, and labeled_measurements */ @TestPlanName("nightly-tally-TC001") - @ParameterizedTest(name = "Physical RHEL: {0} actual sockets -> {1} reported sockets") + @ParameterizedTest(name = "Physical RHEL: {0} starting sockets -> {1} reported sockets") @MethodSource("socketMappingProvider") void test_validate_tally_on_physical_rhel_sockets( - int actualSockets, int expectedReportedSockets) { - String inventoryId = helpers.generateUUIDOfSize(false, 5) + "-" + actualSockets; - String subscriptionManagerId = helpers.generateUUIDOfSize(false, 5) + "-" + actualSockets; - String displayName = - "RHEL Host " + helpers.generateUUIDOfSize(false, 5) + actualSockets + " sockets"; + int startingSockets, int expectedReportedSockets) { + // Given: Org is opted in service.createOptInConfig(orgId); @@ -110,19 +114,20 @@ void test_validate_tally_on_physical_rhel_sockets( Log.info("Initial sockets: %.0f", initialSockets); // And: Create RHEL host - int cores = actualSockets; // 1 core per socket (matches IQE) + int cores = startingSockets; // 1 core per socket (matches IQE) + String displayName = String.format("RHEL-Physical-%dsockets-%dcores", startingSockets, cores); SeededHost host = - hbiSeeder - .rhelHost(orgId) - .inventoryId("inventory-" + actualSockets) - .subscriptionManagerId("subman-" + actualSockets) + hostManager + .createRhsmHost(orgId) .displayName(displayName) + .rhsmFact("RH_PROD", List.of("69")) + .rhsmFact("ARCHITECTURE", "x86_64") .cores(cores) - .sockets(actualSockets) + .sockets(startingSockets) .insert(); - Log.info("Inserted host %s: %d cores, %d sockets", host.hostId(), cores, actualSockets); + Log.info("Inserted host %s: %d cores, %d sockets", host.hostId(), cores, startingSockets); // And: Run tally service.tallyOrg(orgId); @@ -169,6 +174,6 @@ void test_validate_tally_on_physical_rhel_sockets( instance.getMeasurements().get(socketsIndex), String.format( "Labeled measurement should show %d sockets (increased from %d)", - expectedReportedSockets, actualSockets)); + expectedReportedSockets, startingSockets)); } } diff --git a/swatch-tally/ct/java/utils/TallyHbiDbSeeder.java b/swatch-tally/ct/java/utils/TallyHbiDbSeeder.java index 3074a58822..72ff27a186 100644 --- a/swatch-tally/ct/java/utils/TallyHbiDbSeeder.java +++ b/swatch-tally/ct/java/utils/TallyHbiDbSeeder.java @@ -60,6 +60,19 @@ */ public final class TallyHbiDbSeeder { + // separate functionality of the Seeding, DB connection, and HBI into different classes + // Need to be specific about what kind of RHEL host we are creating, so we can use the correct one + // Need to specify what facts per RHEL host ( factory for the RHEL host ) + /*example for Kartik + * public HostBuilder rhelHost(String orgId) { + return new HostBuilder(orgId, true); + } + + public HostBuilder cloudHost(String orgId) { + return new HostBuilder(orgId, false).cloudProvider("aws"); + } + * */ + private final DatabaseService hbiDatabase; // Default values for test hosts @@ -192,6 +205,7 @@ public CloudHostBuilder cloudHost(String orgId) { * Builder for RHEL hosts. Defaults to physical infrastructure; call {@link #cloudProvider} to * create a "RHEL on cloud" host (virtual infrastructure with cloud provider metadata). */ + // the host public class RhelHostBuilder { private final String orgId; private String inventoryId; diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HbiDbConnector.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HbiDbConnector.java new file mode 100644 index 0000000000..6f0725a15a --- /dev/null +++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HbiDbConnector.java @@ -0,0 +1,337 @@ +/* + * Copyright Red Hat, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Red Hat trademarks are not licensed under GPLv3. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.swatch.component.tests.api.hbi; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.redhat.swatch.component.tests.api.db.DatabaseService; +import com.redhat.swatch.component.tests.api.hbi.HostConnector.SeededHost; +import com.redhat.swatch.component.tests.logging.Log; +import com.redhat.swatch.component.tests.utils.AwaitilitySettings; +import com.redhat.swatch.component.tests.utils.AwaitilityUtils; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * HBI database connector - inserts hosts directly into the HBI database. + * + *

This is the fastest approach for component tests but bypasses the Kafka ingestion pipeline. + * + *

Usage: + * + *

+ * HbiDbConnector connector = new HbiDbConnector(hbiDatabase);
+ * SeededHost seeded = connector.seed(hostDefinition);
+ * 
+ */ +public class HbiDbConnector implements HostConnector { + + private final DatabaseService hbiDatabase; + private final ObjectMapper objectMapper = new ObjectMapper(); + private boolean schemaVerified = false; + + public HbiDbConnector(DatabaseService hbiDatabase) { + this.hbiDatabase = Objects.requireNonNull(hbiDatabase, "hbiDatabase is required"); + } + + @Override + public SeededHost seed(Host host) { + + verifySchema(); + + UUID insightsId = + host.getInsightsId() != null ? UUID.fromString(host.getInsightsId()) : UUID.randomUUID(); + OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC); + + // Use provided timestamps or default to now + OffsetDateTime createdOn = host.getCreatedOn() != null ? host.getCreatedOn() : now; + OffsetDateTime modifiedOn = host.getModifiedOn() != null ? host.getModifiedOn() : now; + OffsetDateTime lastCheckIn = host.getLastCheckIn() != null ? host.getLastCheckIn() : now; + + String subscriptionManagerId = host.getSubscriptionManagerId(); // Can be null + + // Validate sockets before INSERT + if (host.getSockets() != null && host.getSockets() == 0) { + throw new IllegalArgumentException( + "Sockets cannot be 0 (would cause division by zero when calculating cores_per_socket)"); + } + + UUID hostId = null; + try (Connection conn = hbiDatabase.getConnection()) { + // Insert host and get the database-generated ID + hostId = insertHost(conn, insightsId, host, createdOn, modifiedOn, lastCheckIn); + insertSystemProfile(conn, hostId, host); + } catch (SQLException e) { + if (hostId != null && hostExists(hostId)) { + // Host already exists, but we failed to insert the system profile + // This is a rare case, but it can happen if the host is being re-inserted + // after a previous failure. + // In this case, we can safely ignore the error and continue. + cleanup(hostId); + } + throw new RuntimeException("Failed to seed host into HBI database", e); + } + + // Use the host ID as the inventory ID + // In HBI, the hosts.id column serves as the inventory identifier. + // There is no separate inventory_id column in the schema. + String inventoryId = hostId.toString(); + + return new SeededHost(hostId, inventoryId, subscriptionManagerId, host.getOrgId()); + } + + @Override + public List seedBatch(List hosts) { + return hosts.stream().map(this::seed).collect(Collectors.toList()); + } + + @Override + public void cleanup(UUID hostId) { + try (Connection conn = hbiDatabase.getConnection()) { + deleteSystemProfile(conn, hostId); + deleteHost(conn, hostId); + Log.debug("Cleaned up host: %s", hostId); + } catch (SQLException e) { + throw new RuntimeException("Failed to cleanup host: " + hostId, e); + } + } + + @Override + public boolean hostExists(UUID hostId) { + String sql = "SELECT 1 FROM hbi.hosts WHERE id = ? LIMIT 1"; + try (Connection conn = hbiDatabase.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, hostId); + var rs = ps.executeQuery(); + return rs.next(); + } catch (SQLException e) { + throw new RuntimeException("Failed to check if host exists: " + hostId, e); + } + } + + // ===== Private Helper Methods ===== + + private void verifySchema() { + if (schemaVerified) { + return; + } + Log.info("Waiting for HBI schema to be ready..."); + AwaitilityUtils.untilIsTrue( + () -> { + try (Connection conn = hbiDatabase.getConnection(); + PreparedStatement ps = + conn.prepareStatement( + "SELECT COUNT(*) FROM information_schema.tables" + + " WHERE table_schema = 'hbi'" + + " AND table_name IN ('hosts', 'system_profiles_static')")) { + var rs = ps.executeQuery(); + return rs.next() && rs.getInt(1) == 2; + } + }, + AwaitilitySettings.using(Duration.ofSeconds(2), Duration.ofSeconds(120)) + .timeoutMessage( + "HBI schema not ready after 120s - migration job may not have completed")); + schemaVerified = true; + Log.info("HBI schema is ready."); + } + + private UUID insertHost( + Connection conn, + UUID insightsId, + Host host, + OffsetDateTime createdOn, + OffsetDateTime modifiedOn, + OffsetDateTime lastCheckIn) + throws SQLException { + + String sql = + """ + INSERT INTO hbi.hosts + (id, org_id, display_name, insights_id, subscription_manager_id, provider_id, + created_on, modified_on, last_check_in, + facts, groups, reporter, reporters) + VALUES + (gen_random_uuid(), ?, ?, ?, ?, ?, + ?, ?, ?, + ?::jsonb, ?::jsonb, ?, ?) + RETURNING id + """; + + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, host.getOrgId()); + ps.setString( + 2, host.getDisplayName() != null ? host.getDisplayName() : "Test Host (auto-generated)"); + ps.setObject(3, insightsId); + ps.setString(4, host.getSubscriptionManagerId()); + ps.setString(5, host.getProviderId()); + ps.setObject(6, createdOn); + ps.setObject(7, modifiedOn); + ps.setObject(8, lastCheckIn); + ps.setString(9, buildFactsJson(host)); + ps.setString(10, "[]"); // groups + ps.setString(11, host.getReporter()); + ps.setArray(12, conn.createArrayOf("varchar", host.getReporters())); + + // Execute query and retrieve the database-generated ID + var rs = ps.executeQuery(); + if (!rs.next()) { + throw new SQLException("Failed to retrieve generated host ID from database"); + } + UUID hostId = (UUID) rs.getObject("id"); + return hostId; + } catch (SQLException e) { + String errorMessage = "Failed to insert host into hbi.hosts"; + if (e.getMessage() != null && e.getMessage().contains("does not exist")) { + errorMessage += + "\n\nSCHEMA ERROR: Required HBI table or column is missing." + + "\nThis usually means:" + + "\n 1. HBI database migrations haven't been run (local: see README for setup)" + + "\n 2. Schema is outdated (EE: check if host-inventory-run-db-migrations job" + + " completed)" + + "\n 3. Wrong database selected (verify you're connecting to 'insights'" + + " database)" + + "\n\nOriginal error: " + + e.getMessage(); + } + throw new SQLException(errorMessage, e); + } + } + + private void insertSystemProfile(Connection conn, UUID hostId, Host host) throws SQLException { + String sql = + """ + INSERT INTO hbi.system_profiles_static + (org_id, host_id, cores_per_socket, number_of_sockets, number_of_cpus, + threads_per_core, infrastructure_type, cloud_provider, arch, is_marketplace, + virtual_host_uuid, host_type) + VALUES + (?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, + ?::uuid, ?) + """; + + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, host.getOrgId()); + ps.setObject(2, hostId); + + // Calculate cores_per_socket if not explicitly set + Integer coresPerSocket = host.getCoresPerSocket(); + + // Set nullable integers + if (coresPerSocket != null) { + ps.setInt(3, coresPerSocket); + } else { + ps.setNull(3, java.sql.Types.INTEGER); + } + + if (host.getSockets() != null) { + ps.setInt(4, host.getSockets()); + } else { + ps.setNull(4, java.sql.Types.INTEGER); + } + + if (host.getCores() != null) { + ps.setInt(5, host.getCores()); + } else { + ps.setNull(5, java.sql.Types.INTEGER); + } + + if (host.getThreadsPerCore() != null) { + ps.setInt(6, host.getThreadsPerCore()); + } else { + ps.setNull(6, java.sql.Types.INTEGER); + } + + ps.setString(7, host.getInfrastructureType()); + ps.setString(8, host.getCloudProvider()); + ps.setString(9, host.getArch()); + + if (host.getIsMarketplace() != null) { + ps.setBoolean(10, host.getIsMarketplace()); + } else { + ps.setNull(10, java.sql.Types.BOOLEAN); + } + + ps.setString(11, host.getVirtualHostUuid()); + ps.setString(12, host.getHostType()); + + ps.executeUpdate(); + } catch (SQLException e) { + String errorMessage = "Failed to insert into hbi.system_profiles_static"; + if (e.getMessage() != null && e.getMessage().contains("does not exist")) { + errorMessage += + "\n\nSCHEMA ERROR: system_profiles_static table or column missing." + + "\nOriginal error: " + + e.getMessage(); + } + throw new SQLException(errorMessage, e); + } + } + + private void deleteSystemProfile(Connection conn, UUID hostId) throws SQLException { + String sql = "DELETE FROM hbi.system_profiles_static WHERE host_id = ?"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, hostId); + ps.executeUpdate(); + } + } + + private void deleteHost(Connection conn, UUID hostId) throws SQLException { + String sql = "DELETE FROM hbi.hosts WHERE id = ?"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, hostId); + ps.executeUpdate(); + } + } + + private String buildFactsJson(Host host) { + Map facts = new HashMap<>(); + + // Add fact groups if they have content + if (!host.getRhsmFacts().isEmpty()) { + facts.put("rhsm", host.getRhsmFacts()); + } + if (!host.getQpcFacts().isEmpty()) { + facts.put("qpc", host.getQpcFacts()); + facts.put("yupana", host.getYupanaFacts()); + } + if (!host.getSatelliteFacts().isEmpty()) { + facts.put("satellite", host.getSatelliteFacts()); + facts.put("yupana", host.getYupanaFacts()); + } + + try { + return objectMapper.writeValueAsString(facts); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize facts to JSON", e); + } + } +} diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/Host.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/Host.java new file mode 100644 index 0000000000..b33d5e4cea --- /dev/null +++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/Host.java @@ -0,0 +1,469 @@ +/* + * Copyright Red Hat, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Red Hat trademarks are not licensed under GPLv3. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.swatch.component.tests.api.hbi; + +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +/** + * Host data container with fluent setters. + * + *

Represents host data that will be seeded into the HBI database. Fields align with the + * InventoryHost query to support different tally normalization scenarios. + * + *

Usage: + * + *

+ * Host baseHost = new Host(orgId)
+ *     .inventoryId("inv-123")
+ *     .cores(4)
+ *     .sockets(2);
+ *
+ * SeededHost seeded = hostManager.qpc(baseHost)
+ *     .awsRhelLarge()
+ *     .displayName("Custom")
+ *     .insert();
+ * 
+ */ +public class Host { + + // ===== Core Identity ===== + private String orgId; + private UUID inventoryId; + private String subscriptionManagerId; + private String insightsId; + private String displayName; + + // ===== System Profile Fields ===== + private String infrastructureType; // "physical" or "virtual" + private Integer coresPerSocket; + private Integer numberOfSockets; + private Integer numberOfCpus; + private Integer threadsPerCore; + private String arch; // e.g., "x86_64" + private String cloudProvider; // e.g., "aws", "azure", null for physical + private String providerId; // Cloud provider instance ID + private Boolean isMarketplace; + private String virtualHostUuid; // Hypervisor UUID (from system_profiles_static) + private String hostType; // e.g., "edge" (filtered by query line 153) + private String account; // Deprecated, but included for completeness + + // ===== Facts (stored as JSONB in HBI) ===== + private Map rhsmFacts = new HashMap<>(); + private Map qpcFacts = new HashMap<>(); + private Map satelliteFacts = new HashMap<>(); + private Map yupanaFacts = new HashMap<>(); + + // ===== Reporter Info ===== + private String reporter = "component-test"; + private String[] reporters = new String[] {"component-test"}; + + // ===== Timestamps ===== + private OffsetDateTime createdOn; + private OffsetDateTime modifiedOn; + private OffsetDateTime lastCheckIn; + + // ===== Additional Fields ===== + private String conversionsActivity; + private String billingModel; + + /** + * Create a new Host with the specified organization ID. + * + * @param orgId the organization ID (required) + */ + public Host(String orgId) { + this.orgId = Objects.requireNonNull(orgId, "orgId is required"); + } + + public Host(Host source) { + + // ===== Core Identity ===== + this.orgId = source.getOrgId(); + this.inventoryId = source.getInventoryId(); + this.subscriptionManagerId = source.getSubscriptionManagerId(); + this.insightsId = source.getInsightsId(); + this.displayName = source.getDisplayName(); + + // ===== System Profile Fields ===== + this.infrastructureType = source.getInfrastructureType(); // "physical" or "virtual" + this.coresPerSocket = source.getCoresPerSocket(); + this.numberOfSockets = source.getSockets(); + this.numberOfCpus = source.getCores(); + this.threadsPerCore = source.getThreadsPerCore(); + this.arch = source.getArch(); // e.g., "x86_64" + this.cloudProvider = source.getCloudProvider(); // e.g., "aws", "azure", null for physical + this.providerId = source.getProviderId(); // Cloud provider instance ID + this.isMarketplace = source.getIsMarketplace(); + this.virtualHostUuid = + source.getVirtualHostUuid(); // Hypervisor UUID (from system_profiles_static) + this.hostType = source.getHostType(); // e.g., "edge" (filtered by query line 153) + this.account = source.getAccount(); // Deprecated, but included for completeness + + // ====== Facts (stored as JSONB in HBI) ====== + this.rhsmFacts = new HashMap<>(source.getRhsmFacts()); + this.qpcFacts = new HashMap<>(source.getQpcFacts()); + this.satelliteFacts = new HashMap<>(source.getSatelliteFacts()); + this.yupanaFacts = new HashMap<>(source.getYupanaFacts()); + + // ===== Reporter Info ===== + // This made need to be appended to the existing reporters array, not replaced. + this.reporter = source.getReporter(); + this.reporters = source.getReporters().clone(); + + // ===== Timestamps ===== + this.createdOn = source.getCreatedOn(); + this.modifiedOn = source.getModifiedOn(); + this.lastCheckIn = source.getLastCheckIn(); + + // ===== Additional Fields ===== + this.conversionsActivity = source.getConversionsActivity(); + this.billingModel = source.getBillingModel(); + } + + // ===== Core Identity Setters ===== + + public Host subscriptionManagerId(String subscriptionManagerId) { + this.subscriptionManagerId = subscriptionManagerId; + return this; + } + + public Host insightsId(String insightsId) { + this.insightsId = insightsId; + return this; + } + + public Host displayName(String displayName) { + this.displayName = displayName; + return this; + } + + public Host orgId(String orgId) { + this.orgId = orgId; + return this; + } + + // ===== System Profile Setters ===== + + public Host infrastructureType(String infrastructureType) { + this.infrastructureType = infrastructureType; + return this; + } + + public Host coresPerSocket(Integer coresPerSocket) { + this.coresPerSocket = coresPerSocket; + return this; + } + + public Host sockets(Integer numberOfSockets) { + this.numberOfSockets = numberOfSockets; + return this; + } + + public Host cores(Integer numberOfCpus) { + this.numberOfCpus = numberOfCpus; + return this; + } + + public Host threadsPerCore(Integer threadsPerCore) { + this.threadsPerCore = threadsPerCore; + return this; + } + + public Host arch(String arch) { + this.arch = arch; + return this; + } + + public Host cloudProvider(String cloudProvider) { + this.cloudProvider = cloudProvider; + return this; + } + + public Host providerId(String providerId) { + this.providerId = providerId; + return this; + } + + public Host isMarketplace(Boolean isMarketplace) { + this.isMarketplace = isMarketplace; + return this; + } + + public Host virtualHostUuid(String virtualHostUuid) { + this.virtualHostUuid = virtualHostUuid; + return this; + } + + public Host hostType(String hostType) { + this.hostType = hostType; + return this; + } + + public Host account(String account) { + this.account = account; + return this; + } + + // ===== Facts Setters ===== + + /** + * Add a RHSM fact (stored in h.facts->'rhsm' in HBI). + * + *

Common RHSM facts: IS_VIRTUAL, RH_PROD, ARCHITECTURE, CORES, SOCKETS, BILLING_MODEL, + * SYSPURPOSE_ROLE, SYSPURPOSE_SLA, SYSPURPOSE_USAGE + */ + public Host rhsmFact(String key, Object value) { + this.rhsmFacts.put(key, value); + return this; + } + + /** + * Add multiple RHSM facts (stored in h.facts->'rhsm' in HBI). + * + *

Common RHSM facts: IS_VIRTUAL, RH_PROD, ARCHITECTURE, CORES, SOCKETS, BILLING_MODEL, + * SYSPURPOSE_ROLE, SYSPURPOSE_SLA, SYSPURPOSE_USAGE + */ + public Host rhsmFacts(Map rhsmFacts) { + this.rhsmFacts.putAll(rhsmFacts); + return this; + } + + /** + * Add a QPC fact (stored in h.facts->'qpc' in HBI). + * + *

Common QPC facts: rh_products_installed, IS_RHEL + */ + public Host qpcFact(String key, Object value) { + this.qpcFacts.put(key, value); + return this; + } + + /** + * Add multiple QPC facts (stored in h.facts->'qpc' in HBI). + * + *

Common QPC facts: rh_products_installed, IS_RHEL + */ + public Host qpcFacts(Map qpcFacts) { + this.qpcFacts.putAll(qpcFacts); + return this; + } + + /** + * Add a Satellite fact (stored in h.facts->'satellite' in HBI). + * + *

Common Satellite facts: virtual_host_uuid, system_purpose_role, system_purpose_sla, + * system_purpose_usage + */ + public Host satelliteFact(String key, Object value) { + this.satelliteFacts.put(key, value); + return this; + } + + /** + * Add multiple Satellite facts (stored in h.facts->'satellite' in HBI). + * + *

Common Satellite facts: virtual_host_uuid, system_purpose_role, system_purpose_sla, + * system_purpose_usage + */ + public Host satelliteFacts(Map satelliteFacts) { + this.satelliteFacts.putAll(satelliteFacts); + return this; + } + + /** + * Add a Yapana fact (stored in h.facts->'yapana' in HBI). + * + *

Common Yapana facts: org_id, account, yupana_host_id, report_slice_id, report_platform_id + */ + public Host yupanaFacts(String key, Object value) { + this.yupanaFacts.put(key, value); + return this; + } + + /** + * Add multiple Yapana facts (stored in h.facts->'yapana' in HBI). + * + *

Common Yapana facts: org_id, account, yupana_host_id, report_slice_id, report_platform_id + */ + public Host yupanaFacts(Map yupanaFacts) { + this.yupanaFacts.putAll(yupanaFacts); + return this; + } + + // ===== Reporter Setters ===== + + public Host reporter(String reporter) { + this.reporter = reporter; + return this; + } + + public Host reporters(String... reporters) { + this.reporters = reporters; + return this; + } + + // ===== Timestamp Setters ===== + + public Host createdOn(OffsetDateTime createdOn) { + this.createdOn = createdOn; + return this; + } + + public Host modifiedOn(OffsetDateTime modifiedOn) { + this.modifiedOn = modifiedOn; + return this; + } + + public Host lastCheckIn(OffsetDateTime lastCheckIn) { + this.lastCheckIn = lastCheckIn; + return this; + } + + // ===== Additional Field Setters ===== + + public Host conversionsActivity(String conversionsActivity) { + this.conversionsActivity = conversionsActivity; + return this; + } + + public Host billingModel(String billingModel) { + this.billingModel = billingModel; + return this; + } + + // ===== Getters ===== + + public String getOrgId() { + return orgId; + } + + public UUID getInventoryId() { + return inventoryId; + } + + public String getSubscriptionManagerId() { + return subscriptionManagerId; + } + + public String getInsightsId() { + return insightsId; + } + + public String getDisplayName() { + return displayName; + } + + public String getInfrastructureType() { + return infrastructureType; + } + + public Integer getCoresPerSocket() { + return coresPerSocket; + } + + public Integer getSockets() { + return numberOfSockets; + } + + public Integer getCores() { + return numberOfCpus; + } + + public Integer getThreadsPerCore() { + return threadsPerCore; + } + + public String getArch() { + return arch; + } + + public String getCloudProvider() { + return cloudProvider; + } + + public String getProviderId() { + return providerId; + } + + public Boolean getIsMarketplace() { + return isMarketplace; + } + + public String getVirtualHostUuid() { + return virtualHostUuid; + } + + public String getHostType() { + return hostType; + } + + public String getAccount() { + return account; + } + + public Map getRhsmFacts() { + return rhsmFacts; + } + + public Map getQpcFacts() { + return qpcFacts; + } + + public Map getSatelliteFacts() { + return satelliteFacts; + } + + public Map getYupanaFacts() { + return yupanaFacts; + } + + public String getReporter() { + return reporter; + } + + public String[] getReporters() { + return reporters; + } + + public OffsetDateTime getCreatedOn() { + return createdOn; + } + + public OffsetDateTime getModifiedOn() { + return modifiedOn; + } + + public OffsetDateTime getLastCheckIn() { + return lastCheckIn; + } + + public String getConversionsActivity() { + return conversionsActivity; + } + + public String getBillingModel() { + return billingModel; + } +} diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HostBuilder.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HostBuilder.java new file mode 100644 index 0000000000..6555489616 --- /dev/null +++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HostBuilder.java @@ -0,0 +1,380 @@ +/* + * Copyright Red Hat, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Red Hat trademarks are not licensed under GPLv3. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.swatch.component.tests.api.hbi; + +import com.redhat.swatch.component.tests.api.hbi.HostConnector.SeededHost; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; + +/** + * Builder for applying templates and customizations before inserting. + * + *

Provides template methods for common infrastructure configurations and override methods for + * specific fields. + */ +public class HostBuilder { + private final HostStateManager manager; + private final Host host; + private static final String RHEL_PRODUCT_ID = "69"; + + HostBuilder(HostStateManager manager, Host host) { + this.manager = manager; + this.host = host; + } + + // ===== Physical RHEL Templates ===== + + /** Apply physical RHEL defaults (infrastructure=physical, IS_VIRTUAL=false, RHEL product). */ + public HostBuilder physicalRhelDefaults() { + host.infrastructureType("physical"); + host.arch("x86_64"); + host.rhsmFact("IS_VIRTUAL", "false"); + host.rhsmFact("RH_PROD", List.of(RHEL_PRODUCT_ID)); + host.rhsmFact("ARCHITECTURE", "x86_64"); + return this; + } + + /** Physical RHEL host with 1 socket, 0 cores. */ + public HostBuilder physicalRhel1Socket0Cores() { + physicalRhelDefaults(); + host.sockets(1); + host.cores(0); + host.displayName(getOrDefault(host.getDisplayName(), generateName("physical", "RHEL", 1, 0))); + return this; + } + + /** Physical RHEL host with 2 sockets, 2 cores. */ + public HostBuilder physicalRhel2Socket2Cores() { + physicalRhelDefaults(); + host.sockets(2); + host.cores(2); + host.displayName(getOrDefault(host.getDisplayName(), generateName("physical", "RHEL", 2, 2))); + return this; + } + + /** Physical RHEL host with 8 sockets, 8 cores. */ + public HostBuilder physicalRhel8Sockets8Cores() { + physicalRhelDefaults(); + host.sockets(8); + host.cores(8); + host.displayName(getOrDefault(host.getDisplayName(), generateName("physical", "RHEL", 8, 8))); + return this; + } + + // ===== AWS RHEL Templates ===== + + /** Apply AWS RHEL defaults (infrastructure=virtual, cloudProvider=aws, RHEL product). */ + public HostBuilder awsRhelDefaults() { + host.infrastructureType("virtual"); + host.cloudProvider("aws"); + host.arch("x86_64"); + host.providerId("i-test-" + getUUIDOfLength(12)); + host.rhsmFact("IS_VIRTUAL", "true"); + host.rhsmFact("RH_PROD", List.of(RHEL_PRODUCT_ID)); + host.rhsmFact("ARCHITECTURE", "x86_64"); + return this; + } + + /** AWS RHEL host - small instance (t3.medium equivalent). */ + public HostBuilder awsRhelSockets1Cores1() { + awsRhelDefaults(); + host.sockets(1); + host.cores(1); + host.displayName(getOrDefault(host.getDisplayName(), generateName("virtual", "RHEL", 1, 1))); + return this; + } + + // ===== Add additional Reporters ===== + + public HostBuilder addRhsmReporter() { + // clear out any existing rhsm facts + host.getRhsmFacts().clear(); + // copies the default rhsm facts from the rhsm host + host.rhsmFacts(new RhsmHost(host.getOrgId()).getRhsmFacts()); + // add the rhsm reporter + host.reporter("rhsm-conduit"); + host.reporters( + Stream.concat(Stream.of("rhsm-conduit"), Arrays.stream(host.getReporters())) + .toArray(String[]::new)); + + return this; + } + + public HostBuilder addSatelliteReporter() { + // clear out any existing satellite facts + host.getSatelliteFacts().clear(); + // copies the default rhsm satellite from the satellite host + host.satelliteFacts(new SatelliteHost("").getSatelliteFacts()); + // add the satellite reporter + host.reporter("satellite"); + host.reporters( + Stream.concat(Stream.of("satellite"), Arrays.stream(host.getReporters())) + .toArray(String[]::new)); + + return this; + } + + public HostBuilder addQpcReporter() { + // clear out any existing qpc facts + host.getQpcFacts().clear(); + // copies the default qpc facts from the qpc host + host.qpcFacts(new QpcHost("").getQpcFacts()); + // add the qpc reporter + host.reporter("discovery"); + host.reporters( + Stream.concat(Stream.of("satellite"), Arrays.stream(host.getReporters())) + .toArray(String[]::new)); + + return this; + } + + public HostBuilder addReporters(ArrayList reporters) { + if (reporters != null) { + if (reporters.contains("rhsm-conduit")) { + addRhsmReporter(); + } + if (reporters.contains("satellite")) { + addSatelliteReporter(); + } + if (reporters.contains("qpc")) { + addQpcReporter(); + } + } + + return this; + } + + // ===== Override Methods (Always Override) ===== + + public HostBuilder orgId(String orgId) { + host.orgId(orgId); + return this; + } + + public HostBuilder displayName(String displayName) { + host.displayName(displayName); + return this; + } + + public HostBuilder subscriptionManagerId(String subscriptionManagerId) { + host.subscriptionManagerId(subscriptionManagerId); + return this; + } + + public HostBuilder cores(Integer cores) { + host.cores(cores); + return this; + } + + public HostBuilder sockets(Integer sockets) { + host.sockets(sockets); + return this; + } + + public HostBuilder arch(String arch) { + host.arch(arch); + return this; + } + + public HostBuilder infrastructureType(String infrastructureType) { + host.infrastructureType(infrastructureType); + return this; + } + + public HostBuilder cloudProvider(String cloudProvider) { + host.cloudProvider(cloudProvider); + return this; + } + + public HostBuilder providerId(String providerId) { + host.providerId(providerId); + return this; + } + + public HostBuilder rhsmFact(String key, Object value) { + host.rhsmFact(key, value); + return this; + } + + public HostBuilder qpcFact(String key, Object value) { + host.qpcFact(key, value); + return this; + } + + public HostBuilder satelliteFact(String key, Object value) { + host.satelliteFact(key, value); + return this; + } + + // ===== QPC Fact Convenience Methods ===== + + /** + * Set the list of Red Hat products installed (detected by QPC scan). + * + *

Maps to h.facts->'qpc'->>'rh_products_installed' in the InventoryHost query. + * + *

Common product IDs: "69" (RHEL), "479" (RHEL for SAP), etc. + */ + public HostBuilder qpcProductsInstalled(List productIds) { + host.qpcFact("rh_products_installed", productIds); + return this; + } + + /** + * Set whether QPC detected this host as RHEL. + * + *

Maps to h.facts->'qpc'->>'IS_RHEL' in the InventoryHost query. + */ + public HostBuilder isRhel(boolean isRhel) { + host.qpcFact("IS_RHEL", String.valueOf(isRhel)); + return this; + } + + // ===== Satellite Fact Convenience Methods ===== + + /** + * Set the virtual host UUID (hypervisor UUID) in Satellite facts. + * + *

Maps to h.facts->'satellite'->>'virtual_host_uuid' in the InventoryHost query. + */ + public HostBuilder hypervisorUuid(String uuid) { + host.satelliteFact("virtual_host_uuid", uuid); + return this; + } + + /** + * Set the system purpose role in Satellite facts. + * + *

Maps to h.facts->'satellite'->>'system_purpose_role' in the InventoryHost query. + * + *

Common values: "Red Hat Enterprise Linux Server", "Red Hat Enterprise Linux Workstation" + */ + public HostBuilder systemPurposeRole(String role) { + host.satelliteFact("system_purpose_role", role); + return this; + } + + /** + * Set the system purpose SLA in Satellite facts. + * + *

Maps to h.facts->'satellite'->>'system_purpose_sla' in the InventoryHost query. + * + *

Common values: "Premium", "Standard", "Self-Support" + */ + public HostBuilder systemPurposeSla(String sla) { + host.satelliteFact("system_purpose_sla", sla); + return this; + } + + /** + * Set the system purpose usage in Satellite facts. + * + *

Maps to h.facts->'satellite'->>'system_purpose_usage' in the InventoryHost query. + * + *

Common values: "Production", "Development/Test", "Disaster Recovery" + */ + public HostBuilder systemPurposeUsage(String usage) { + host.satelliteFact("system_purpose_usage", usage); + return this; + } + + // ===== Insert (Final Action) ===== + + /** + * Finalize and insert the host into HBI. + * + *

Calculates derived fields (cores_per_socket) and seeds via connector. + * + * @return information about the seeded host + */ + public SeededHost insert() { + // if Salellite or QPC host set Yupana facts + if (host instanceof SatelliteHost || host instanceof QpcHost) { + setYupanaFacts(); + } + + // Calculate derived fields + if (host.getCores() != null && host.getSockets() != null && host.getSockets() > 0) { + if (host.getCoresPerSocket() == null) { + host.coresPerSocket(host.getCores() / host.getSockets()); + } + } + + // Seed via manager (which tracks the host) + return manager.seed(host); + } + + private String getOrDefault(String value, String defaultValue) { + return value != null ? value : defaultValue; + } + + private String getUUIDOfLength(int length) { + return UUID.randomUUID().toString().substring(0, length); + } + + private String generateName( + String infrastType, String instanceType, int socketCount, int coreCount) { + String prefix = + StringUtils.capitalize(infrastType.substring(0, Math.min(3, infrastType.length()))); + return String.format( + "%s-%s-sockets%d-cores%d-%s", + prefix, instanceType, socketCount, coreCount, getUUIDOfLength(5)); + } + + private void setYupanaFacts() { + // Apply Yupana fact defaults (only if not already set) + + if (!host.getYupanaFacts().containsKey("org_id")) { + host.yupanaFacts("org_id", host.getOrgId()); + } + + if (!host.getYupanaFacts().containsKey("source")) { + if (host instanceof SatelliteHost) { + host.yupanaFacts("source", "satellite"); + } else if (host instanceof QpcHost) { + host.yupanaFacts("source", "discovery"); + } else { + host.yupanaFacts("source", ""); + } + } + + if (!host.getYupanaFacts().containsKey("account")) { + if (host.getAccount() == null) { + host.yupanaFacts("account", UUID.randomUUID().toString()); + } else { + host.yupanaFacts("account", host.getAccount()); + } + } + if (!host.getYupanaFacts().containsKey("yupana_host_id")) { + host.yupanaFacts("yupana_host_id", UUID.randomUUID().toString()); + } + if (!host.getYupanaFacts().containsKey("report_slice_id")) { + host.yupanaFacts("report_slice_id", UUID.randomUUID().toString()); + } + if (!host.getYupanaFacts().containsKey("report_platform_id")) { + host.yupanaFacts("report_platform_id", UUID.randomUUID().toString()); + } + } +} diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HostConnector.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HostConnector.java new file mode 100644 index 0000000000..3aba874920 --- /dev/null +++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HostConnector.java @@ -0,0 +1,80 @@ +/* + * Copyright Red Hat, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Red Hat trademarks are not licensed under GPLv3. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.swatch.component.tests.api.hbi; + +import java.util.List; +import java.util.UUID; + +/** + * Interface for seeding hosts into HBI via different mechanisms. + * + *

Implementations include: + * + *

    + *
  • {@link HbiDbConnector} - Direct database insertion (fast, for component tests) + *
  • KafkaConnector - Kafka message (realistic, tests full ingestion pipeline) + *
+ */ +public interface HostConnector { + + /** + * Seed a single host. + * + * @param host the host data to seed + * @return information about the seeded host + */ + SeededHost seed(Host host); + + /** + * Seed multiple hosts in batch. + * + *

Implementations may optimize batch operations (e.g., bulk INSERT, batch Kafka send). + * + * @param hosts the list of hosts to seed + * @return list of seeded host information + */ + List seedBatch(List hosts); + + /** + * Delete a host by ID. + * + * @param hostId the host UUID to delete + */ + void cleanup(UUID hostId); + + /** + * Check if a host exists. + * + * @param hostId the host UUID to check + * @return true if the host exists, false otherwise + */ + boolean hostExists(UUID hostId); + + /** + * Record of a seeded host for test assertions. + * + * @param hostId the UUID assigned to the host in HBI + * @param inventoryId the inventory ID + * @param subscriptionManagerId the subscription manager ID + * @param orgId the organization ID + */ + record SeededHost(UUID hostId, String inventoryId, String subscriptionManagerId, String orgId) {} +} diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HostStateManager.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HostStateManager.java new file mode 100644 index 0000000000..221ee25461 --- /dev/null +++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/HostStateManager.java @@ -0,0 +1,300 @@ +/* + * Copyright Red Hat, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Red Hat trademarks are not licensed under GPLv3. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.swatch.component.tests.api.hbi; + +import com.redhat.swatch.component.tests.api.hbi.HostConnector.SeededHost; +import com.redhat.swatch.component.tests.logging.Log; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * Host state manager - orchestrates host seeding with templates and lifecycle tracking. + * + *

Provides entry points for different reporter types (QPC, RHSM, Satellite), applies templates, + * tracks seeded hosts, and handles cleanup. + * + *

Usage: + * + *

+ * HostStateManager hostManager = new HostStateManager(connector);
+ *
+ * // Create base host
+ * QpcHost qpcHost = new QpcHost(orgId)
+ *     .cores(4)
+ *     .sockets(2);
+ *
+ * // Apply template and insert
+ * SeededHost seeded = hostManager.qpc(qpcHost)
+ *     .awsRhelLarge()
+ *     .displayName("Custom")
+ *     .insert();
+ *
+ * // Cleanup (or automatic in @AfterEach)
+ * hostManager.cleanupAll();
+ * 
+ */ +public class HostStateManager { + + private final HostConnector connector; + private final List trackedHostIds = new ArrayList<>(); + + public HostStateManager(HostConnector connector) { + this.connector = Objects.requireNonNull(connector, "connector is required"); + } + + // ===== Entry Points for Different Reporter Types ===== + + /** + * Start building a host with pre-configured host. + * + *

Takes an existing Host and copies it, then applies template overrides. + * + * @param host the pre-configured host with NO fields already set + * @return builder for applying template overrides + */ + public HostBuilder createHost(Host host) { + return new HostBuilder(this, host); + } + + /** + * Start building a host from scratch. + * + *

Creates a new Host with orgId and applies template defaults. + * + * @param orgId the organization ID + * @return builder for applying template defaults + */ + public HostBuilder createHost(String orgId) { + return new HostBuilder(this, new Host(orgId)); + } + + // ===== QPC Reporter ===== + + /** + * Start building a QPC-reported host with pre-configured host. + * + *

Takes an existing Host and copies it, then applies QPC defaults for any unset fields. + * + *

QPC defaults applied (only if not already set): + * + *

    + *
  • reporter: "qpc" + *
  • reporters: ["qpc"] + *
  • arch: "x86_64" + *
+ * + * @param host the pre-configured host with fields already set + * @return builder for applying templates and additional overrides + */ + public HostBuilder createQpcHost(Host host) { + return new HostBuilder(this, new QpcHost(host)); + } + + /** + * Start building a QPC-reported host from scratch. + * + *

Creates a new Host with orgId and applies all QPC defaults. + * + *

Defaults applied: + * + *

    + *
  • reporter: "qpc" + *
  • reporters: ["qpc"] + *
  • arch: "x86_64" + *
+ * + * @param orgId the organization ID + * @return builder for applying templates + */ + public HostBuilder createQpcHost(String orgId) { + return new HostBuilder(this, new QpcHost(orgId)); + } + + // ===== Satellite Reporter ===== + + /** + * Start building a Satellite-reported host with pre-configured host. + * + *

Takes an existing Host and copies it, then applies Satellite defaults for any unset fields. + * + *

Use this when you want to pre-configure fields that will be preserved through template + * application: + * + *

+   * Host preConfigured = new Host(orgId)
+   *     .inventoryId("inv-123")
+   *     .subscriptionManagerId("subman-456")
+   *     .satelliteFact("virtual_host_uuid", "hypervisor-123");
+   *
+   * hostManager.satellite(preConfigured)
+   *     .awsRhelLarge()
+   *     .insert();
+   * 
+ * + *

Satellite defaults applied (only if not already set): + * + *

    + *
  • reporter: "satellite" + *
  • reporters: ["satellite"] + *
  • arch: "x86_64" + *
  • system_purpose_role: "Red Hat Enterprise Linux Server" + *
  • system_purpose_sla: "Premium" + *
  • system_purpose_usage: "Production" + *
+ * + *

Satellite-specific facts from InventoryHost query: + * + *

    + *
  • h.facts->'satellite'->>'virtual_host_uuid' (hypervisor UUID) + *
  • h.facts->'satellite'->>'system_purpose_role' + *
  • h.facts->'satellite'->>'system_purpose_sla' + *
  • h.facts->'satellite'->>'system_purpose_usage' + *
+ * + * @param host the pre-configured host with fields already set + * @return builder for applying templates and additional overrides + */ + public HostBuilder createSatelliteHost(Host host) { + return new HostBuilder(this, new SatelliteHost(host)); + } + + /** + * Start building a Satellite-reported host from scratch. + * + *

Creates a new Host with orgId and applies all Satellite defaults. + * + *

Use this for simple tests where you don't need to pre-configure fields: + * + *

+   * hostManager.satellite(orgId)
+   *     .physicalRhel4Socket()
+   *     .displayName("Custom Name")
+   *     .insert();
+   * 
+ * + *

Defaults applied: + * + *

    + *
  • reporter: "satellite" + *
  • reporters: ["satellite"] + *
  • arch: "x86_64" + *
  • system_purpose_role: "Red Hat Enterprise Linux Server" + *
  • system_purpose_sla: "Premium" + *
  • system_purpose_usage: "Production" + *
+ * + * @param orgId the organization ID + * @return builder for applying templates + */ + public HostBuilder createSatelliteHost(String orgId) { + return new HostBuilder(this, new SatelliteHost(orgId)); + } + + // ===== RHSM CONDUIT Reporter ===== + + public HostBuilder createRhsmHost(Host host) { + return new HostBuilder(this, new RhsmHost(host)); + } + + public HostBuilder createRhsmHost(String orgId) { + return new HostBuilder(this, new RhsmHost(orgId)); + } + + // ===== Internal Seed Method (called by HostBuilder.insert()) ===== + + SeededHost seed(Host host) { + SeededHost seeded = connector.seed(host); + trackedHostIds.add(seeded.hostId()); + Log.info( + "Seeded host: %s (inventoryId=%s, orgId=%s)", + seeded.hostId(), seeded.inventoryId(), seeded.orgId()); + return seeded; + } + + // ===== Cleanup Methods ===== + + /** + * Delete all hosts tracked by this manager. + * + *

Call this in @AfterEach to ensure cleanup even on test failure. + */ + public void cleanupAll() { + List toCleanup = new ArrayList<>(trackedHostIds); + int successCount = 0; + int failCount = 0; + + for (UUID hostId : toCleanup) { + try { + connector.cleanup(hostId); + trackedHostIds.remove(hostId); + successCount++; + } catch (Exception e) { + failCount++; + Log.error("Failed to cleanup host %s: %s", hostId, e.getMessage()); + } + } + + if (successCount > 0) { + Log.info("Cleaned up %d host(s)", successCount); + } + if (failCount > 0) { + Log.warn( + "Failed to cleanup %d host(s). See Errors above for Host id's that are still remaining in the DB.", + failCount); + } + } + + /** + * Delete a specific host by ID. + * + * @param hostId the host UUID to delete + */ + public void cleanup(UUID hostId) { + try { + connector.cleanup(hostId); + trackedHostIds.remove(hostId); + Log.info("Cleaned up host: %s", hostId); + } catch (Exception e) { + Log.error("Failed to cleanup host %s: %s", hostId, e.getMessage()); + } + } + + /** + * Check if a host exists in the database. + * + * @param hostId the host UUID to check + * @return true if the host exists, false otherwise + */ + public boolean hostExists(UUID hostId) { + return connector.hostExists(hostId); + } + + /** + * Get the count of tracked hosts. + * + * @return number of hosts currently tracked + */ + public int getTrackedCount() { + return trackedHostIds.size(); + } +} diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/QpcHost.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/QpcHost.java new file mode 100644 index 0000000000..2d21ab78b6 --- /dev/null +++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/QpcHost.java @@ -0,0 +1,79 @@ +/* + * Copyright Red Hat, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Red Hat trademarks are not licensed under GPLv3. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.swatch.component.tests.api.hbi; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +/** + * Host data container with fluent setters. + * + *

Represents host data that will be seeded into the HBI database. Fields align with the + * InventoryHost query to support different tally normalization scenarios. + * + *

Usage: + * + *

+ * Host baseHost = new Host(orgId)
+ *     .inventoryId("inv-123")
+ *     .cores(4)
+ *     .sockets(2);
+ *
+ * SeededHost seeded = hostManager.qpc(baseHost)
+ *     .awsRhelLarge()
+ *     .displayName("Custom")
+ *     .insert();
+ * 
+ */ +public class QpcHost extends Host { + public QpcHost(String orgId) { + super(orgId); + applyQpcFactDefaults(); + } + + public QpcHost(Host host) { + super(host); + if (!(host instanceof QpcHost)) { + applyQpcFactDefaults(); + } + } + + private void applyQpcFactDefaults() { + OffsetDateTime today = OffsetDateTime.now(ZoneOffset.UTC); + + // Apply reporter defaults (only if not set) + // Need an appending strategy for multiple reporters + if (getReporter() == null || "component-test".equals(getReporter())) { + reporter("qpc"); + } + // Need an appending strategy for multiple reporters + if (getReporters() == null + || (getReporters().length == 1 && "component-test".equals(getReporters()[0]))) { + reporters("qpc"); + } + + if (getArch() == null) { + arch("x86_64"); + } + + qpcFact("last_discovered", today.toString()); + } +} diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/RhsmHost.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/RhsmHost.java new file mode 100644 index 0000000000..cd55265142 --- /dev/null +++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/RhsmHost.java @@ -0,0 +1,96 @@ +/* + * Copyright Red Hat, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Red Hat trademarks are not licensed under GPLv3. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.swatch.component.tests.api.hbi; + +/** + * Host data container with fluent setters. + * + *

Represents host data that will be seeded into the HBI database. Fields align with the + * InventoryHost query to support different tally normalization scenarios. + * + *

Usage: + * + *

+ * Host baseHost = new Host(orgId)
+ *     .inventoryId("inv-123")
+ *     .cores(4)
+ *     .sockets(2);
+ *
+ * SeededHost seeded = hostManager.qpc(baseHost)
+ *     .awsRhelLarge()
+ *     .displayName("Custom")
+ *     .insert();
+ * 
+ */ +public class RhsmHost extends Host { + public RhsmHost(String orgId) { + super(orgId); + applyRhsmFactDefaults(); + } + + public RhsmHost(Host source) { + super(source); + if (!(source instanceof RhsmHost)) { + applyRhsmFactDefaults(); + } + } + + /** + * Add a RHSM fact (stored in h.facts->'rhsm' in HBI). + * + *

Common RHSM facts: IS_VIRTUAL, RH_PROD, ARCHITECTURE, CORES, SOCKETS, BILLING_MODEL, + * SYSPURPOSE_ROLE, SYSPURPOSE_SLA, SYSPURPOSE_USAGE + */ + public void applyRhsmFactDefaults() { + /// Apply reporter defaults (only if not set) + if (getReporter() == null || "component-test".equals(getReporter())) { + reporter("rhsm-conduit"); + } + if (getReporters() == null + || (getReporters().length == 1 && "component-test".equals(getReporters()[0]))) { + reporters("rhsm-conduit"); + } + if (getArch() == null) { + arch("x86_64"); + } + + // Only set RHSM facts if not already present + // Memory: need to determine what this is and if we need it. All the facts are set to 1 + if (!getRhsmFacts().containsKey("org_id")) { + rhsmFact("org_id", getOrgId()); + } + + if (!getRhsmFacts().containsKey("IS_VIRTUAL")) { + rhsmFact("IS_VIRTUAL", "false"); + } + + if (!getRhsmFacts().containsKey("ARCHITECTURE")) { + rhsmFact("ARCHITECTURE", "x86_64"); + } + } + + @Override + public Host orgId(String orgId) { + super.orgId(orgId); + rhsmFact("org_id", orgId); + return this; + } +} diff --git a/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/SatelliteHost.java b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/SatelliteHost.java new file mode 100644 index 0000000000..8beab8fd41 --- /dev/null +++ b/swatch-test-framework/src/main/java/com/redhat/swatch/component/tests/api/hbi/SatelliteHost.java @@ -0,0 +1,106 @@ +/* + * Copyright Red Hat, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Red Hat trademarks are not licensed under GPLv3. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.swatch.component.tests.api.hbi; + +/** + * Host data container with fluent setters. + * + *

Represents host data that will be seeded into the HBI database. Fields align with the + * InventoryHost query to support different tally normalization scenarios. + * + *

Usage: + * + *

+ * Host baseHost = new Host(orgId)
+ *     .inventoryId("inv-123")
+ *     .cores(4)
+ *     .sockets(2);
+ *
+ * SeededHost seeded = hostManager.qpc(baseHost)
+ *     .awsRhelLarge()
+ *     .displayName("Custom")
+ *     .insert();
+ * 
+ */ +public class SatelliteHost extends Host { + + public SatelliteHost(String orgId) { + super(orgId); + // Sets the default values for Satellite hosts + applySatelliteDefaults(); + } + + // // Sets this hosts values to the host passed in + public SatelliteHost(Host host) { + super(host); + if (!(host instanceof SatelliteHost)) { + applySatelliteDefaults(); + } + } + + /** + * Apply Satellite-specific defaults (only if not already set). + * + *

Based on common values from FactNormalizerTest and production usage. + * + *

Defaults applied: + * + *

    + *
  • reporter: "satellite" (if not set) + *
  • reporters: ["satellite"] (if not set) + *
  • arch: "x86_64" (if not set) + *
  • system_purpose_role: "Red Hat Enterprise Linux Server" (if not set in satellite facts) + *
  • system_purpose_sla: "Premium" (if not set in satellite facts) + *
  • system_purpose_usage: "Production" (if not set in satellite facts) + *
+ * + * @param host the host to apply defaults to + */ + private void applySatelliteDefaults() { + // Apply reporter defaults (only if not set) + + if (getReporter() == null || "component-test".equals(getReporter())) { + reporter("satellite"); // Since you can have multiple reporters, which is set to this value? + } + + if (getReporters() == null + || (getReporters().length == 1 && "component-test".equals(getReporters()[0]))) { + reporters("satellite"); + } + + if (getArch() == null) { + arch("x86_64"); // verify with data + } + + // Apply Satellite fact defaults (only if not already set) + if (!getSatelliteFacts().containsKey("system_purpose_role")) { + satelliteFact("system_purpose_role", "Red Hat Enterprise Linux Server"); + } + + if (!getSatelliteFacts().containsKey("system_purpose_sla")) { + satelliteFact("system_purpose_sla", "Premium"); + } + + if (!getSatelliteFacts().containsKey("system_purpose_usage")) { + satelliteFact("system_purpose_usage", "Production"); + } + } +}