@oneuptime/common 12.0.34 → 13.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/GlobalConfig.ts +12 -12
- package/Models/DatabaseModels/InventoryItem.ts +44 -1
- package/Models/DatabaseModels/NetworkDevice.ts +126 -0
- package/Server/EnvironmentConfig.ts +47 -16
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Redis.ts +8 -8
- package/Server/Infrastructure/Status.ts +10 -4
- package/Server/Services/DatabaseService.ts +65 -13
- package/Server/Services/InventoryItemService.ts +31 -1
- package/Server/Services/MonitorTemplateService.ts +140 -19
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
- package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
- package/Server/Services/NetworkDeviceService.ts +69 -0
- package/Server/Services/UserNotificationRuleService.ts +76 -25
- package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
- package/Server/Types/Database/QueryUtil.ts +56 -0
- package/Server/Types/Markdown.ts +93 -1
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
- package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
- package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
- package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
- package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
- package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
- package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
- package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
- package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
- package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
- package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
- package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
- package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
- package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
- package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
- package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
- package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
- package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
- package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
- package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
- package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
- package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
- package/Tests/Models/InventoryItemStatus.test.ts +147 -0
- package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
- package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
- package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
- package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
- package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
- package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
- package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
- package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
- package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
- package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
- package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
- package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
- package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
- package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
- package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
- package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
- package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
- package/Tests/Server/Types/Markdown.test.ts +194 -1
- package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
- package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
- package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
- package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
- package/Tests/Server/Utils/EventLoop.test.ts +359 -0
- package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
- package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
- package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
- package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
- package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
- package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
- package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
- package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
- package/Tests/Types/JSONFunctions.test.ts +40 -0
- package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
- package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
- package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
- package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
- package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
- package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
- package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
- package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
- package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
- package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
- package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
- package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
- package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
- package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
- package/Types/Dashboard/DashboardVariable.ts +8 -0
- package/Types/JSON.ts +4 -0
- package/Types/JSONFunctions.ts +16 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
- package/Types/SerializableObjectDictionary.ts +4 -0
- package/Types/Telemetry/InventoryLiveness.ts +56 -0
- package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
- package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
- package/UI/esbuild-config.js +6 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
- package/Utils/Dashboard/LabelVariable.ts +152 -0
- package/Utils/Dashboard/VariableUrlState.ts +27 -5
- package/Utils/MetricUnitUtil.ts +34 -0
- package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
- package/Utils/Monitor/MetricValueFormatter.ts +342 -0
- package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
- package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
- package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +44 -12
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Redis.js +8 -8
- package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
- package/build/dist/Server/Infrastructure/Status.js +10 -4
- package/build/dist/Server/Infrastructure/Status.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +58 -13
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +28 -1
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
- package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Types/Markdown.js +75 -0
- package/build/dist/Server/Types/Markdown.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
- package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
- package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
- package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
- package/build/dist/Types/JSON.js +1 -0
- package/build/dist/Types/JSON.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +13 -0
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +4 -0
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
- package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
- package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
- package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
- package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
- package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
- package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
- package/build/dist/Utils/MetricUnitUtil.js +26 -0
- package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
- package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
- package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
- package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
- package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/test-setup.sh +9 -9
- package/tsconfig.json +51 -11
- package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
|
@@ -117,6 +117,7 @@ import NetworkDeviceAutoImportRuleEngineService, {
|
|
|
117
117
|
AUTO_IMPORT_SWEEP_LOCK_TIMEOUT_MS,
|
|
118
118
|
ExistingHostnamesByProjectId,
|
|
119
119
|
ImportAttemptBudgetsByProjectId,
|
|
120
|
+
MAX_CONSECUTIVE_MONITOR_CREATE_FAILURES,
|
|
120
121
|
MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
121
122
|
MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
122
123
|
MAX_RESULT_AGE_IN_HOURS,
|
|
@@ -139,6 +140,7 @@ import NetworkDeviceDiscoveryScan, {
|
|
|
139
140
|
} from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
|
|
140
141
|
import { DiscoveryScanSnmpConfig } from "../../../Utils/NetworkDiscovery/SnmpScanConfigUtil";
|
|
141
142
|
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
143
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
142
144
|
import ObjectID from "../../../Types/ObjectID";
|
|
143
145
|
import OneUptimeDate from "../../../Types/Date";
|
|
144
146
|
import MonitorSteps from "../../../Types/Monitor/MonitorSteps";
|
|
@@ -587,6 +589,7 @@ describe("NetworkDeviceAutoImportRuleEngineService.processCompletedScan", () =>
|
|
|
587
589
|
const unmatchedRow: DiscoveredNetworkDevice = makeHost({
|
|
588
590
|
ipAddress: "192.168.1.5",
|
|
589
591
|
sysName: "printer-01",
|
|
592
|
+
isAlreadyRegistered: true,
|
|
590
593
|
});
|
|
591
594
|
|
|
592
595
|
scanFindOneByMock.mockResolvedValue(
|
|
@@ -611,6 +614,10 @@ describe("NetworkDeviceAutoImportRuleEngineService.processCompletedScan", () =>
|
|
|
611
614
|
expect(createMock).toHaveBeenCalledTimes(1);
|
|
612
615
|
expect(monitorFindByMock).not.toHaveBeenCalled();
|
|
613
616
|
expect(monitorTemplateFindByMock).not.toHaveBeenCalled();
|
|
617
|
+
expect(scanFindOneByMock.mock.calls[0]![0].props).toEqual({
|
|
618
|
+
isRoot: true,
|
|
619
|
+
ignoreHooks: true,
|
|
620
|
+
});
|
|
614
621
|
const device: NetworkDevice = createdDevice(0);
|
|
615
622
|
// The address is the hostname AND the dedup key downstream.
|
|
616
623
|
expect(device.hostname).toBe("10.0.0.5");
|
|
@@ -813,6 +820,66 @@ describe("NetworkDeviceAutoImportRuleEngineService.processCompletedScan", () =>
|
|
|
813
820
|
).toBe(TEMPLATE_DEVICE_ID);
|
|
814
821
|
});
|
|
815
822
|
|
|
823
|
+
/*
|
|
824
|
+
* ISSUE #3548. A template's custom fields are the DEFAULTS its monitors
|
|
825
|
+
* are born with, and this is the path that made that worth having: a
|
|
826
|
+
* discovery scan importing a thousand devices used to leave a thousand
|
|
827
|
+
* monitors with every custom field empty, fillable only one monitor at a
|
|
828
|
+
* time.
|
|
829
|
+
*
|
|
830
|
+
* Driven through the engine rather than the builder alone for the same
|
|
831
|
+
* reason as the name test above — the values only reach the create call if
|
|
832
|
+
* `customFields` is still in this service's template select, and a dropped
|
|
833
|
+
* select column is indistinguishable from a template nobody set defaults
|
|
834
|
+
* on. Two hosts, because the cached template serves the whole estate: a
|
|
835
|
+
* shallow copy would hand every monitor in the run the same jsonb object,
|
|
836
|
+
* so one later edit would reach the rest of the fleet and the template.
|
|
837
|
+
*/
|
|
838
|
+
it("gives every monitor in one run its own copy of the template's custom field defaults", async () => {
|
|
839
|
+
const template: MonitorTemplate = makeTemplate({
|
|
840
|
+
customFields: { Vendor: "Cisco", Thresholds: { cpu: 80 } },
|
|
841
|
+
});
|
|
842
|
+
scanFindOneByMock.mockResolvedValue(
|
|
843
|
+
makeScan({
|
|
844
|
+
discoveredDevices: [
|
|
845
|
+
makeHost(),
|
|
846
|
+
makeHost({ ipAddress: "10.0.0.6", sysName: "core-switch-02" }),
|
|
847
|
+
],
|
|
848
|
+
}),
|
|
849
|
+
);
|
|
850
|
+
ruleFindByMock.mockResolvedValue([
|
|
851
|
+
makeRule({ monitorTemplateId: TEMPLATE_ID }),
|
|
852
|
+
]);
|
|
853
|
+
monitorTemplateFindByMock.mockResolvedValue([template]);
|
|
854
|
+
|
|
855
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
856
|
+
|
|
857
|
+
expect(result).toMatchObject({ devicesCreated: 2, monitorsCreated: 2 });
|
|
858
|
+
|
|
859
|
+
const firstMonitor: Monitor = provisionedMonitor(0);
|
|
860
|
+
const secondMonitor: Monitor = provisionedMonitor(1);
|
|
861
|
+
|
|
862
|
+
for (const monitor of [firstMonitor, secondMonitor]) {
|
|
863
|
+
expect(monitor.customFields).toEqual({
|
|
864
|
+
Vendor: "Cisco",
|
|
865
|
+
Thresholds: { cpu: 80 },
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
expect(firstMonitor.customFields).not.toBe(secondMonitor.customFields);
|
|
870
|
+
expect(firstMonitor.customFields?.["Thresholds"]).not.toBe(
|
|
871
|
+
secondMonitor.customFields?.["Thresholds"],
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
(firstMonitor.customFields!["Thresholds"] as JSONObject)["cpu"] = 10;
|
|
875
|
+
expect(
|
|
876
|
+
(secondMonitor.customFields!["Thresholds"] as JSONObject)["cpu"],
|
|
877
|
+
).toBe(80);
|
|
878
|
+
expect((template.customFields!["Thresholds"] as JSONObject)["cpu"]).toBe(
|
|
879
|
+
80,
|
|
880
|
+
);
|
|
881
|
+
});
|
|
882
|
+
|
|
816
883
|
/*
|
|
817
884
|
* ISSUE #3486. "Default Monitor Name" is optional, and a template that
|
|
818
885
|
* leaves it blank names what it provisions after the DEVICE and nothing
|
|
@@ -1314,7 +1381,89 @@ describe("NetworkDeviceAutoImportRuleEngineService.processCompletedScan", () =>
|
|
|
1314
1381
|
expect(monitorCreateMock).not.toHaveBeenCalled();
|
|
1315
1382
|
});
|
|
1316
1383
|
|
|
1317
|
-
|
|
1384
|
+
/*
|
|
1385
|
+
* OneUptime/oneuptime#3643, the report this whole block exists for.
|
|
1386
|
+
*
|
|
1387
|
+
* An estate of 501 devices that a previous run already imported, and a
|
|
1388
|
+
* rule whose Monitor Template no longer resolves. The template is a
|
|
1389
|
+
* property of the RULE: it fails for the first device and for all 500
|
|
1390
|
+
* after it, and no amount of running again changes that.
|
|
1391
|
+
*
|
|
1392
|
+
* What the customer saw was the engine re-proving that 500 times, one
|
|
1393
|
+
* wasted monitor budget slot at a time, until the budget ran out — then
|
|
1394
|
+
* reporting "Stopped at the run cap - run again to continue", with no hint
|
|
1395
|
+
* of what was actually wrong. What it must do instead is settle the
|
|
1396
|
+
* verdict once, count every monitor that is genuinely missing, and say
|
|
1397
|
+
* why.
|
|
1398
|
+
*/
|
|
1399
|
+
it("settles an unresolvable template once instead of re-failing it per device", async () => {
|
|
1400
|
+
const hosts: Array<DiscoveredNetworkDevice> = Array.from(
|
|
1401
|
+
{ length: MAX_MONITORS_PER_AUTO_IMPORT_RUN + 1 },
|
|
1402
|
+
(_value: unknown, index: number): DiscoveredNetworkDevice => {
|
|
1403
|
+
return makeHost({
|
|
1404
|
+
ipAddress: `10.2.${Math.floor(index / 256)}.${index % 256}`,
|
|
1405
|
+
sysName: `missing-template-switch-${index}`,
|
|
1406
|
+
});
|
|
1407
|
+
},
|
|
1408
|
+
);
|
|
1409
|
+
const devices: Array<NetworkDevice> = hosts.map(
|
|
1410
|
+
(host: DiscoveredNetworkDevice): NetworkDevice => {
|
|
1411
|
+
return makeExistingDevice({
|
|
1412
|
+
id: ObjectID.generate(),
|
|
1413
|
+
hostname: host.ipAddress,
|
|
1414
|
+
name: host.sysName,
|
|
1415
|
+
});
|
|
1416
|
+
},
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
1420
|
+
makeRule({
|
|
1421
|
+
ipMatchTarget: "10.0.0.0/8",
|
|
1422
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
1423
|
+
}),
|
|
1424
|
+
);
|
|
1425
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
1426
|
+
deviceFindByMock.mockResolvedValue(devices);
|
|
1427
|
+
monitorTemplateFindByMock.mockResolvedValue([]);
|
|
1428
|
+
mockRunNowScans([makeScan({ discoveredDevices: hosts })]);
|
|
1429
|
+
|
|
1430
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
1431
|
+
|
|
1432
|
+
expect(result).toMatchObject({
|
|
1433
|
+
hostsMatched: hosts.length,
|
|
1434
|
+
hostsSkippedAlreadyRegistered: hosts.length,
|
|
1435
|
+
devicesCreated: 0,
|
|
1436
|
+
monitorsCreated: 0,
|
|
1437
|
+
// Every device really is missing its monitor, so every one is counted.
|
|
1438
|
+
monitorsFailed: hosts.length,
|
|
1439
|
+
monitorProvisioningHalted: true,
|
|
1440
|
+
/*
|
|
1441
|
+
* Nothing was cut short by a cap, so nothing is waiting for another
|
|
1442
|
+
* run. This is the misleading half of the customer's dialog.
|
|
1443
|
+
*/
|
|
1444
|
+
isTruncated: false,
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
expect(result.monitorFailureReasons).toEqual([
|
|
1448
|
+
"This rule's Monitor Template could not be loaded. It may have been deleted, moved to another project, or changed to a monitor type other than Network Device. Edit the rule and select a Network Device Monitor Template.",
|
|
1449
|
+
]);
|
|
1450
|
+
expect(monitorCreateMock).not.toHaveBeenCalled();
|
|
1451
|
+
|
|
1452
|
+
/*
|
|
1453
|
+
* And it costs one log line and one budget slot, not 500 of each. The
|
|
1454
|
+
* per-device race check is an unindexed search of the monitor step
|
|
1455
|
+
* payload; running it 500 times to reach the same verdict is the other
|
|
1456
|
+
* half of what made this run expensive.
|
|
1457
|
+
*/
|
|
1458
|
+
expect(
|
|
1459
|
+
loggerErrorMock.mock.calls.filter((call: Array<unknown>): boolean => {
|
|
1460
|
+
return String(call[0]).includes("is missing, deleted, or not valid");
|
|
1461
|
+
}),
|
|
1462
|
+
).toHaveLength(1);
|
|
1463
|
+
expect(monitorFindByMock.mock.calls.length).toBeLessThanOrEqual(2);
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
it("reports the same unresolvable template on a dry run without a cap", async () => {
|
|
1318
1467
|
const hosts: Array<DiscoveredNetworkDevice> = Array.from(
|
|
1319
1468
|
{ length: MAX_MONITORS_PER_AUTO_IMPORT_RUN + 1 },
|
|
1320
1469
|
(_value: unknown, index: number): DiscoveredNetworkDevice => {
|
|
@@ -1349,115 +1498,409 @@ describe("NetworkDeviceAutoImportRuleEngineService.processCompletedScan", () =>
|
|
|
1349
1498
|
|
|
1350
1499
|
expect(result).toMatchObject({
|
|
1351
1500
|
monitorsWouldCreate: 0,
|
|
1352
|
-
monitorsFailed:
|
|
1353
|
-
|
|
1501
|
+
monitorsFailed: hosts.length,
|
|
1502
|
+
monitorProvisioningHalted: true,
|
|
1503
|
+
isTruncated: false,
|
|
1354
1504
|
isDryRun: true,
|
|
1355
1505
|
});
|
|
1356
1506
|
expect(monitorCreateMock).not.toHaveBeenCalled();
|
|
1357
1507
|
});
|
|
1358
|
-
});
|
|
1359
1508
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1509
|
+
/*
|
|
1510
|
+
* The other shape of the same report: the template resolves fine, and
|
|
1511
|
+
* every create fails the same way anyway — a plan limit, criteria naming
|
|
1512
|
+
* a monitor status that no longer exists, a database refusing writes.
|
|
1513
|
+
* Indistinguishable from a per-host failure on the first device, so the
|
|
1514
|
+
* engine tries, and stops once the evidence is decisive.
|
|
1515
|
+
*/
|
|
1516
|
+
it("stops provisioning after the same create failure repeats for every device", async () => {
|
|
1517
|
+
const hosts: Array<DiscoveredNetworkDevice> = Array.from(
|
|
1518
|
+
{ length: MAX_MONITORS_PER_AUTO_IMPORT_RUN + 1 },
|
|
1519
|
+
(_value: unknown, index: number): DiscoveredNetworkDevice => {
|
|
1520
|
+
return makeHost({
|
|
1521
|
+
ipAddress: `10.3.${Math.floor(index / 256)}.${index % 256}`,
|
|
1522
|
+
sysName: `systemic-switch-${index}`,
|
|
1523
|
+
});
|
|
1524
|
+
},
|
|
1525
|
+
);
|
|
1526
|
+
const devices: Array<NetworkDevice> = hosts.map(
|
|
1527
|
+
(host: DiscoveredNetworkDevice): NetworkDevice => {
|
|
1528
|
+
return makeExistingDevice({
|
|
1529
|
+
id: ObjectID.generate(),
|
|
1530
|
+
hostname: host.ipAddress,
|
|
1531
|
+
name: host.sysName,
|
|
1532
|
+
});
|
|
1533
|
+
},
|
|
1534
|
+
);
|
|
1374
1535
|
|
|
1375
|
-
|
|
1536
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
1537
|
+
makeRule({
|
|
1538
|
+
ipMatchTarget: "10.0.0.0/8",
|
|
1539
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
1540
|
+
}),
|
|
1541
|
+
);
|
|
1542
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
1543
|
+
deviceFindByMock.mockResolvedValue(devices);
|
|
1544
|
+
monitorFindByMock.mockResolvedValue([]);
|
|
1545
|
+
monitorFindOneByMock.mockResolvedValue(null);
|
|
1546
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
1547
|
+
monitorCreateMock.mockRejectedValue(
|
|
1548
|
+
new BadDataException(
|
|
1549
|
+
"You have reached the maximum allowed monitor limit for the free plan.",
|
|
1550
|
+
),
|
|
1551
|
+
);
|
|
1552
|
+
mockRunNowScans([makeScan({ discoveredDevices: hosts })]);
|
|
1376
1553
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1554
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
1555
|
+
|
|
1556
|
+
expect(monitorCreateMock).toHaveBeenCalledTimes(
|
|
1557
|
+
MAX_CONSECUTIVE_MONITOR_CREATE_FAILURES,
|
|
1558
|
+
);
|
|
1559
|
+
expect(result).toMatchObject({
|
|
1560
|
+
monitorsCreated: 0,
|
|
1561
|
+
monitorsFailed: hosts.length,
|
|
1562
|
+
monitorProvisioningHalted: true,
|
|
1563
|
+
isTruncated: false,
|
|
1564
|
+
});
|
|
1565
|
+
expect(result.monitorFailureReasons).toEqual([
|
|
1566
|
+
"You have reached the maximum allowed monitor limit for the free plan.",
|
|
1567
|
+
]);
|
|
1383
1568
|
});
|
|
1384
|
-
expect(createMock).toHaveBeenCalledTimes(1);
|
|
1385
|
-
expect(createdDevice(0).hostname).toBe("10.0.0.5");
|
|
1386
|
-
});
|
|
1387
1569
|
|
|
1388
|
-
describe("the create-failure protocol", () => {
|
|
1389
1570
|
/*
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1393
|
-
* — one retry under the address-suffixed fallback settles it.
|
|
1571
|
+
* The guard has to stay narrow, or it becomes a new way to lose monitors.
|
|
1572
|
+
* Failures that differ are per-host failures: every device still gets its
|
|
1573
|
+
* try.
|
|
1394
1574
|
*/
|
|
1395
|
-
it("
|
|
1396
|
-
|
|
1397
|
-
|
|
1575
|
+
it("keeps trying every device when the failures are not identical", async () => {
|
|
1576
|
+
const hosts: Array<DiscoveredNetworkDevice> = Array.from(
|
|
1577
|
+
{ length: 30 },
|
|
1578
|
+
(_value: unknown, index: number): DiscoveredNetworkDevice => {
|
|
1579
|
+
return makeHost({
|
|
1580
|
+
ipAddress: `10.4.0.${index}`,
|
|
1581
|
+
sysName: `varied-switch-${index}`,
|
|
1582
|
+
});
|
|
1583
|
+
},
|
|
1584
|
+
);
|
|
1585
|
+
const devices: Array<NetworkDevice> = hosts.map(
|
|
1586
|
+
(host: DiscoveredNetworkDevice): NetworkDevice => {
|
|
1587
|
+
return makeExistingDevice({
|
|
1588
|
+
id: ObjectID.generate(),
|
|
1589
|
+
hostname: host.ipAddress,
|
|
1590
|
+
name: host.sysName,
|
|
1591
|
+
});
|
|
1592
|
+
},
|
|
1398
1593
|
);
|
|
1399
|
-
createMock
|
|
1400
|
-
.mockRejectedValueOnce(
|
|
1401
|
-
new BadDataException("Network Device with this name already exists."),
|
|
1402
|
-
)
|
|
1403
|
-
.mockResolvedValueOnce({});
|
|
1404
|
-
// The address re-check finds nothing: the name really was the problem.
|
|
1405
|
-
deviceFindOneByMock.mockResolvedValue(null);
|
|
1406
1594
|
|
|
1407
|
-
|
|
1595
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
1596
|
+
makeRule({
|
|
1597
|
+
ipMatchTarget: "10.0.0.0/8",
|
|
1598
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
1599
|
+
}),
|
|
1600
|
+
);
|
|
1601
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
1602
|
+
deviceFindByMock.mockResolvedValue(devices);
|
|
1603
|
+
monitorFindByMock.mockResolvedValue([]);
|
|
1604
|
+
monitorFindOneByMock.mockResolvedValue(null);
|
|
1605
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
1408
1606
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1607
|
+
let attempt: number = 0;
|
|
1608
|
+
monitorCreateMock.mockImplementation((): Promise<Monitor> => {
|
|
1609
|
+
attempt++;
|
|
1610
|
+
return Promise.reject(new Error(`name collision on device ${attempt}`));
|
|
1611
|
+
});
|
|
1612
|
+
mockRunNowScans([makeScan({ discoveredDevices: hosts })]);
|
|
1613
|
+
|
|
1614
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
1415
1615
|
|
|
1616
|
+
expect(monitorCreateMock).toHaveBeenCalledTimes(hosts.length);
|
|
1416
1617
|
expect(result).toMatchObject({
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
hostsSkippedAlreadyRegistered: 0,
|
|
1618
|
+
monitorsFailed: hosts.length,
|
|
1619
|
+
monitorProvisioningHalted: false,
|
|
1420
1620
|
});
|
|
1621
|
+
// Distinct reasons are reported, bounded.
|
|
1622
|
+
expect(result.monitorFailureReasons).toHaveLength(3);
|
|
1421
1623
|
});
|
|
1422
1624
|
|
|
1423
1625
|
/*
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1626
|
+
* The guard must never cost an established estate its monitors. Once this
|
|
1627
|
+
* run has provisioned even one, the create pipeline is known to work, and
|
|
1628
|
+
* everything after it is a per-host failure however many of them there are
|
|
1629
|
+
* and however alike they read.
|
|
1427
1630
|
*/
|
|
1428
|
-
it("
|
|
1429
|
-
|
|
1430
|
-
|
|
1631
|
+
it("never gives up once the run has created a monitor at all", async () => {
|
|
1632
|
+
const hosts: Array<DiscoveredNetworkDevice> = Array.from(
|
|
1633
|
+
{ length: MAX_CONSECUTIVE_MONITOR_CREATE_FAILURES * 3 },
|
|
1634
|
+
(_value: unknown, index: number): DiscoveredNetworkDevice => {
|
|
1635
|
+
return makeHost({
|
|
1636
|
+
ipAddress: `10.7.0.${index}`,
|
|
1637
|
+
sysName: `established-switch-${index}`,
|
|
1638
|
+
});
|
|
1639
|
+
},
|
|
1431
1640
|
);
|
|
1432
|
-
|
|
1433
|
-
|
|
1641
|
+
const devices: Array<NetworkDevice> = hosts.map(
|
|
1642
|
+
(host: DiscoveredNetworkDevice): NetworkDevice => {
|
|
1643
|
+
return makeExistingDevice({
|
|
1644
|
+
id: ObjectID.generate(),
|
|
1645
|
+
hostname: host.ipAddress,
|
|
1646
|
+
name: host.sysName,
|
|
1647
|
+
});
|
|
1648
|
+
},
|
|
1434
1649
|
);
|
|
1435
|
-
// The address re-check finds a device: someone imported it meanwhile.
|
|
1436
|
-
deviceFindOneByMock.mockResolvedValue({
|
|
1437
|
-
id: new ObjectID("44444444-4444-4444-8444-444444444444"),
|
|
1438
|
-
} as unknown as NetworkDevice);
|
|
1439
1650
|
|
|
1440
|
-
|
|
1651
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
1652
|
+
makeRule({
|
|
1653
|
+
ipMatchTarget: "10.0.0.0/8",
|
|
1654
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
1655
|
+
}),
|
|
1656
|
+
);
|
|
1657
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
1658
|
+
deviceFindByMock.mockResolvedValue(devices);
|
|
1659
|
+
monitorFindByMock.mockResolvedValue([]);
|
|
1660
|
+
monitorFindOneByMock.mockResolvedValue(null);
|
|
1661
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
1441
1662
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1663
|
+
let call: number = 0;
|
|
1664
|
+
monitorCreateMock.mockImplementation(
|
|
1665
|
+
({ data }: { data: Monitor }): Promise<Monitor> => {
|
|
1666
|
+
call++;
|
|
1667
|
+
if (call === 1) {
|
|
1668
|
+
return Promise.resolve(data);
|
|
1669
|
+
}
|
|
1670
|
+
return Promise.reject(new Error("name already in use"));
|
|
1671
|
+
},
|
|
1672
|
+
);
|
|
1673
|
+
mockRunNowScans([makeScan({ discoveredDevices: hosts })]);
|
|
1674
|
+
|
|
1675
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
1676
|
+
|
|
1677
|
+
expect(monitorCreateMock).toHaveBeenCalledTimes(hosts.length);
|
|
1444
1678
|
expect(result).toMatchObject({
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
devicesFailed: 0,
|
|
1679
|
+
monitorsCreated: 1,
|
|
1680
|
+
monitorsFailed: hosts.length - 1,
|
|
1681
|
+
monitorProvisioningHalted: false,
|
|
1449
1682
|
});
|
|
1450
1683
|
});
|
|
1451
|
-
});
|
|
1452
1684
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1685
|
+
it("lets one success reset the systemic-failure streak", async () => {
|
|
1686
|
+
const hosts: Array<DiscoveredNetworkDevice> = Array.from(
|
|
1687
|
+
{ length: MAX_CONSECUTIVE_MONITOR_CREATE_FAILURES * 2 - 1 },
|
|
1688
|
+
(_value: unknown, index: number): DiscoveredNetworkDevice => {
|
|
1689
|
+
return makeHost({
|
|
1690
|
+
ipAddress: `10.5.0.${index}`,
|
|
1691
|
+
sysName: `recovering-switch-${index}`,
|
|
1692
|
+
});
|
|
1693
|
+
},
|
|
1694
|
+
);
|
|
1695
|
+
const devices: Array<NetworkDevice> = hosts.map(
|
|
1696
|
+
(host: DiscoveredNetworkDevice): NetworkDevice => {
|
|
1697
|
+
return makeExistingDevice({
|
|
1698
|
+
id: ObjectID.generate(),
|
|
1699
|
+
hostname: host.ipAddress,
|
|
1700
|
+
name: host.sysName,
|
|
1701
|
+
});
|
|
1702
|
+
},
|
|
1703
|
+
);
|
|
1704
|
+
|
|
1705
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
1706
|
+
makeRule({
|
|
1707
|
+
ipMatchTarget: "10.0.0.0/8",
|
|
1708
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
1709
|
+
}),
|
|
1710
|
+
);
|
|
1711
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
1712
|
+
deviceFindByMock.mockResolvedValue(devices);
|
|
1713
|
+
monitorFindByMock.mockResolvedValue([]);
|
|
1714
|
+
monitorFindOneByMock.mockResolvedValue(null);
|
|
1715
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
1716
|
+
|
|
1717
|
+
/*
|
|
1718
|
+
* Fail one short of the threshold, succeed once, then fail one short
|
|
1719
|
+
* again. Without the reset the second stretch would inherit the first
|
|
1720
|
+
* and halt the run.
|
|
1721
|
+
*/
|
|
1722
|
+
let call: number = 0;
|
|
1723
|
+
monitorCreateMock.mockImplementation(
|
|
1724
|
+
({ data }: { data: Monitor }): Promise<Monitor> => {
|
|
1725
|
+
call++;
|
|
1726
|
+
if (call === MAX_CONSECUTIVE_MONITOR_CREATE_FAILURES) {
|
|
1727
|
+
return Promise.resolve(data);
|
|
1728
|
+
}
|
|
1729
|
+
return Promise.reject(new Error("transient write failure"));
|
|
1730
|
+
},
|
|
1731
|
+
);
|
|
1732
|
+
mockRunNowScans([makeScan({ discoveredDevices: hosts })]);
|
|
1733
|
+
|
|
1734
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
1735
|
+
|
|
1736
|
+
expect(monitorCreateMock).toHaveBeenCalledTimes(hosts.length);
|
|
1737
|
+
expect(result).toMatchObject({
|
|
1738
|
+
monitorsCreated: 1,
|
|
1739
|
+
monitorsFailed: hosts.length - 1,
|
|
1740
|
+
monitorProvisioningHalted: false,
|
|
1741
|
+
});
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
/*
|
|
1745
|
+
* Overlapping scans of the same subnet are normal, and a host in both is
|
|
1746
|
+
* ONE device with one missing monitor. The short circuit has to keep the
|
|
1747
|
+
* same (device, template) bookkeeping a real attempt does, or the report
|
|
1748
|
+
* inflates itself once per scan that mentions the host.
|
|
1749
|
+
*/
|
|
1750
|
+
it("counts a halted device once even when two scans carry the same host", async () => {
|
|
1751
|
+
const host: DiscoveredNetworkDevice = makeHost({
|
|
1752
|
+
ipAddress: "10.6.0.1",
|
|
1753
|
+
sysName: "overlapping-switch",
|
|
1754
|
+
});
|
|
1755
|
+
const device: NetworkDevice = makeExistingDevice({
|
|
1756
|
+
id: ObjectID.generate(),
|
|
1757
|
+
hostname: host.ipAddress,
|
|
1758
|
+
name: host.sysName,
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
1762
|
+
makeRule({
|
|
1763
|
+
ipMatchTarget: "10.0.0.0/8",
|
|
1764
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
1765
|
+
}),
|
|
1766
|
+
);
|
|
1767
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
1768
|
+
deviceFindByMock.mockResolvedValue([device]);
|
|
1769
|
+
monitorTemplateFindByMock.mockResolvedValue([]);
|
|
1770
|
+
mockRunNowScans([
|
|
1771
|
+
makeScan({ discoveredDevices: [host] }),
|
|
1772
|
+
makeScan({ discoveredDevices: [host] }),
|
|
1773
|
+
]);
|
|
1774
|
+
|
|
1775
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
1776
|
+
|
|
1777
|
+
expect(result).toMatchObject({
|
|
1778
|
+
hostsMatched: 2,
|
|
1779
|
+
monitorsFailed: 1,
|
|
1780
|
+
monitorProvisioningHalted: true,
|
|
1781
|
+
});
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
it("records why a device could not be imported", async () => {
|
|
1785
|
+
scanFindOneByMock.mockResolvedValue(
|
|
1786
|
+
makeScan({ discoveredDevices: [makeHost()] }),
|
|
1787
|
+
);
|
|
1788
|
+
ruleFindByMock.mockResolvedValue([makeRule()]);
|
|
1789
|
+
deviceFindOneByMock.mockResolvedValue(null);
|
|
1790
|
+
createMock.mockRejectedValue(
|
|
1791
|
+
new BadDataException("Network device name is too long."),
|
|
1792
|
+
);
|
|
1793
|
+
|
|
1794
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
1795
|
+
|
|
1796
|
+
expect(result).toMatchObject({ devicesCreated: 0, devicesFailed: 1 });
|
|
1797
|
+
expect(result?.deviceFailureReasons).toEqual([
|
|
1798
|
+
"Network device name is too long.",
|
|
1799
|
+
]);
|
|
1800
|
+
});
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
/*
|
|
1804
|
+
* An over-long "address" is junk that would fail the varchar(100)
|
|
1805
|
+
* hostname on every pass, identically, forever — so it is refused before
|
|
1806
|
+
* any counting rather than logged as a devicesFailed per worker tick.
|
|
1807
|
+
*/
|
|
1808
|
+
it("skips a host whose address could never be a hostname, before any counting", async () => {
|
|
1809
|
+
scanFindOneByMock.mockResolvedValue(
|
|
1810
|
+
makeScan({
|
|
1811
|
+
discoveredDevices: [
|
|
1812
|
+
makeHost({ ipAddress: "9".repeat(101) }),
|
|
1813
|
+
makeHost(),
|
|
1814
|
+
],
|
|
1815
|
+
}),
|
|
1816
|
+
);
|
|
1817
|
+
|
|
1818
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
1819
|
+
|
|
1820
|
+
// The junk row was never evaluated, let alone matched or attempted.
|
|
1821
|
+
expect(result).toMatchObject({
|
|
1822
|
+
hostsEvaluated: 1,
|
|
1823
|
+
hostsMatched: 1,
|
|
1824
|
+
devicesCreated: 1,
|
|
1825
|
+
devicesFailed: 0,
|
|
1826
|
+
});
|
|
1827
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
1828
|
+
expect(createdDevice(0).hostname).toBe("10.0.0.5");
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
describe("the create-failure protocol", () => {
|
|
1832
|
+
/*
|
|
1833
|
+
* Names are unique per project while addresses are not, and two devices
|
|
1834
|
+
* legitimately sharing a sysName is common on real estates. When the
|
|
1835
|
+
* ADDRESS is still free after a failed create, the name was the problem
|
|
1836
|
+
* — one retry under the address-suffixed fallback settles it.
|
|
1837
|
+
*/
|
|
1838
|
+
it("retries a name collision once under the fallback name", async () => {
|
|
1839
|
+
scanFindOneByMock.mockResolvedValue(
|
|
1840
|
+
makeScan({ discoveredDevices: [makeHost()] }),
|
|
1841
|
+
);
|
|
1842
|
+
createMock
|
|
1843
|
+
.mockRejectedValueOnce(
|
|
1844
|
+
new BadDataException("Network Device with this name already exists."),
|
|
1845
|
+
)
|
|
1846
|
+
.mockResolvedValueOnce({});
|
|
1847
|
+
// The address re-check finds nothing: the name really was the problem.
|
|
1848
|
+
deviceFindOneByMock.mockResolvedValue(null);
|
|
1849
|
+
|
|
1850
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
1851
|
+
|
|
1852
|
+
expect(createMock).toHaveBeenCalledTimes(2);
|
|
1853
|
+
// Same device both times; only the name changed.
|
|
1854
|
+
expect(createdDevice(0).name).toBe("core-switch-01");
|
|
1855
|
+
expect(createdDevice(1).name).toBe("core-switch-01 (10.0.0.5)");
|
|
1856
|
+
expect(createdDevice(1).name!.endsWith(" (10.0.0.5)")).toBe(true);
|
|
1857
|
+
expect(createdDevice(1).hostname).toBe("10.0.0.5");
|
|
1858
|
+
|
|
1859
|
+
expect(result).toMatchObject({
|
|
1860
|
+
devicesCreated: 1,
|
|
1861
|
+
devicesFailed: 0,
|
|
1862
|
+
hostsSkippedAlreadyRegistered: 0,
|
|
1863
|
+
});
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
/*
|
|
1867
|
+
* Losing a race to a concurrent import of the SAME host must read as
|
|
1868
|
+
* "already registered" — retrying under a new name here would spawn a
|
|
1869
|
+
* renamed twin of a device that now exists.
|
|
1870
|
+
*/
|
|
1871
|
+
it("reads a lost create race as already-registered, with no rename retry", async () => {
|
|
1872
|
+
scanFindOneByMock.mockResolvedValue(
|
|
1873
|
+
makeScan({ discoveredDevices: [makeHost()] }),
|
|
1874
|
+
);
|
|
1875
|
+
createMock.mockRejectedValueOnce(
|
|
1876
|
+
new BadDataException("Network Device with this name already exists."),
|
|
1877
|
+
);
|
|
1878
|
+
// The address re-check finds a device: someone imported it meanwhile.
|
|
1879
|
+
deviceFindOneByMock.mockResolvedValue({
|
|
1880
|
+
id: new ObjectID("44444444-4444-4444-8444-444444444444"),
|
|
1881
|
+
} as unknown as NetworkDevice);
|
|
1882
|
+
|
|
1883
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
1884
|
+
|
|
1885
|
+
expect(createMock).toHaveBeenCalledTimes(1);
|
|
1886
|
+
expect(deviceFindOneByMock.mock.calls[0]![0].select.projectId).toBe(true);
|
|
1887
|
+
expect(result).toMatchObject({
|
|
1888
|
+
hostsMatched: 1,
|
|
1889
|
+
hostsSkippedAlreadyRegistered: 1,
|
|
1890
|
+
devicesCreated: 0,
|
|
1891
|
+
devicesFailed: 0,
|
|
1892
|
+
});
|
|
1893
|
+
});
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
/*
|
|
1897
|
+
* The cap turns "a typo'd rule meets a huge subnet" into a few paced
|
|
1898
|
+
* worker ticks instead of one burst of thousands of creates — and the
|
|
1899
|
+
* UNSTAMPED marker is what makes the next tick resume where this one
|
|
1900
|
+
* stopped, with idempotency skipping everything already created.
|
|
1901
|
+
*/
|
|
1902
|
+
it("stops at the device cap and leaves the marker unstamped so the next tick resumes", async () => {
|
|
1903
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeHostsBeyondTheCap();
|
|
1461
1904
|
|
|
1462
1905
|
scanFindOneByMock.mockResolvedValue(makeScan({ discoveredDevices: hosts }));
|
|
1463
1906
|
ruleFindByMock.mockResolvedValue([
|
|
@@ -1818,6 +2261,8 @@ describe("NetworkDeviceAutoImportRuleEngineService.applyRuleToCompletedScans", (
|
|
|
1818
2261
|
]);
|
|
1819
2262
|
expect(rereadCall.query.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
1820
2263
|
expect(rereadCall.select.discoveredDevices).toBe(true);
|
|
2264
|
+
expect(rereadCall.props).toEqual({ isRoot: true, ignoreHooks: true });
|
|
2265
|
+
expect(listCall.props).toEqual({ isRoot: true });
|
|
1821
2266
|
});
|
|
1822
2267
|
|
|
1823
2268
|
it("skips a scan that vanished between the stub listing and the re-read", async () => {
|
|
@@ -2062,6 +2507,244 @@ describe("NetworkDeviceAutoImportRuleEngineService.applyRuleToCompletedScans", (
|
|
|
2062
2507
|
});
|
|
2063
2508
|
});
|
|
2064
2509
|
|
|
2510
|
+
describe("re-import after deleting discovered devices or monitors (issue #3560)", () => {
|
|
2511
|
+
it.each([false, true])(
|
|
2512
|
+
"uses current inventory when a processed scan still marks a deleted device registered (dry run: %s)",
|
|
2513
|
+
async (isDryRun: boolean) => {
|
|
2514
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
2515
|
+
makeRule({ monitorTemplateId: TEMPLATE_ID }),
|
|
2516
|
+
);
|
|
2517
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
2518
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
2519
|
+
mockRunNowScans([
|
|
2520
|
+
makeScan({
|
|
2521
|
+
completedAt: new Date("2020-01-01T00:00:00.000Z"),
|
|
2522
|
+
autoImportProcessedAt: OneUptimeDate.getCurrentDate(),
|
|
2523
|
+
discoveredDevices: [makeHost({ isAlreadyRegistered: true })],
|
|
2524
|
+
}),
|
|
2525
|
+
]);
|
|
2526
|
+
|
|
2527
|
+
const result: AutoImportRuleRunResult = await runRule(isDryRun);
|
|
2528
|
+
|
|
2529
|
+
expect(result).toMatchObject({
|
|
2530
|
+
hostsMatched: 1,
|
|
2531
|
+
hostsSkippedAlreadyRegistered: 0,
|
|
2532
|
+
devicesCreated: isDryRun ? 0 : 1,
|
|
2533
|
+
monitorsCreated: isDryRun ? 0 : 1,
|
|
2534
|
+
monitorsWouldCreate: isDryRun ? 1 : 0,
|
|
2535
|
+
devicesFailed: 0,
|
|
2536
|
+
monitorsFailed: 0,
|
|
2537
|
+
matchedIpAddressSample: ["10.0.0.5"],
|
|
2538
|
+
isTruncated: false,
|
|
2539
|
+
isDryRun,
|
|
2540
|
+
});
|
|
2541
|
+
expect(createMock).toHaveBeenCalledTimes(isDryRun ? 0 : 1);
|
|
2542
|
+
expect(monitorCreateMock).toHaveBeenCalledTimes(isDryRun ? 0 : 1);
|
|
2543
|
+
expect(scanUpdateMock).toHaveBeenCalledTimes(isDryRun ? 0 : 1);
|
|
2544
|
+
|
|
2545
|
+
if (!isDryRun) {
|
|
2546
|
+
const device: NetworkDevice = createdDevice(0);
|
|
2547
|
+
expect(device.hostname).toBe("10.0.0.5");
|
|
2548
|
+
expect(device.snmpCommunityString).toBe("public");
|
|
2549
|
+
expect(device.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
2550
|
+
expect(
|
|
2551
|
+
provisionedMonitor(0).autoProvisionedNetworkDeviceId?.toString(),
|
|
2552
|
+
).toBe(device.id?.toString());
|
|
2553
|
+
expect(scanUpdateMock.mock.calls[0]![0].data).toEqual({
|
|
2554
|
+
discoveredDevices: [makeHost({ isAlreadyRegistered: true })],
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
},
|
|
2558
|
+
);
|
|
2559
|
+
|
|
2560
|
+
it("imports a deleted device from fresh automatic results with a stale registration flag", async () => {
|
|
2561
|
+
scanFindOneByMock.mockResolvedValue(
|
|
2562
|
+
makeScan({
|
|
2563
|
+
discoveredDevices: [makeHost({ isAlreadyRegistered: true })],
|
|
2564
|
+
}),
|
|
2565
|
+
);
|
|
2566
|
+
|
|
2567
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
2568
|
+
|
|
2569
|
+
expect(result).toMatchObject({
|
|
2570
|
+
devicesCreated: 1,
|
|
2571
|
+
hostsSkippedAlreadyRegistered: 0,
|
|
2572
|
+
});
|
|
2573
|
+
expect(
|
|
2574
|
+
scanUpdateMock.mock.calls[0]![0].data.autoImportProcessedAt,
|
|
2575
|
+
).toBeInstanceOf(Date);
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
it.each([false, true])(
|
|
2579
|
+
"restores a deleted template monitor while retaining its registered device (dry run: %s)",
|
|
2580
|
+
async (isDryRun: boolean) => {
|
|
2581
|
+
const existingDevice: NetworkDevice = makeExistingDevice();
|
|
2582
|
+
deviceFindByMock.mockResolvedValue([existingDevice]);
|
|
2583
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
2584
|
+
makeRule({ monitorTemplateId: TEMPLATE_ID }),
|
|
2585
|
+
);
|
|
2586
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
2587
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
2588
|
+
mockRunNowScans([
|
|
2589
|
+
makeScan({
|
|
2590
|
+
autoImportProcessedAt: OneUptimeDate.getCurrentDate(),
|
|
2591
|
+
discoveredDevices: [makeHost({ isAlreadyRegistered: true })],
|
|
2592
|
+
}),
|
|
2593
|
+
]);
|
|
2594
|
+
|
|
2595
|
+
const result: AutoImportRuleRunResult = await runRule(isDryRun);
|
|
2596
|
+
|
|
2597
|
+
expect(result).toMatchObject({
|
|
2598
|
+
hostsSkippedAlreadyRegistered: 1,
|
|
2599
|
+
devicesCreated: 0,
|
|
2600
|
+
monitorsCreated: isDryRun ? 0 : 1,
|
|
2601
|
+
monitorsWouldCreate: isDryRun ? 1 : 0,
|
|
2602
|
+
monitorsSkippedAlreadyExisting: 0,
|
|
2603
|
+
});
|
|
2604
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
2605
|
+
expect(scanUpdateMock).not.toHaveBeenCalled();
|
|
2606
|
+
if (!isDryRun) {
|
|
2607
|
+
expect(
|
|
2608
|
+
provisionedMonitor(0).autoProvisionedNetworkDeviceId?.toString(),
|
|
2609
|
+
).toBe(existingDevice.id?.toString());
|
|
2610
|
+
}
|
|
2611
|
+
},
|
|
2612
|
+
);
|
|
2613
|
+
|
|
2614
|
+
it.each([false, true])(
|
|
2615
|
+
"deduplicates stale registered rows within and across scans (dry run: %s)",
|
|
2616
|
+
async (isDryRun: boolean) => {
|
|
2617
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
2618
|
+
makeRule({ monitorTemplateId: TEMPLATE_ID }),
|
|
2619
|
+
);
|
|
2620
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
2621
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
2622
|
+
mockRunNowScans([
|
|
2623
|
+
makeScan({
|
|
2624
|
+
discoveredDevices: [
|
|
2625
|
+
makeHost({ isAlreadyRegistered: true }),
|
|
2626
|
+
makeHost({ ipAddress: " 10.0.0.5 ", isAlreadyRegistered: false }),
|
|
2627
|
+
],
|
|
2628
|
+
}),
|
|
2629
|
+
makeScan({
|
|
2630
|
+
id: ObjectID.generate(),
|
|
2631
|
+
discoveredDevices: [makeHost({ isAlreadyRegistered: true })],
|
|
2632
|
+
}),
|
|
2633
|
+
]);
|
|
2634
|
+
|
|
2635
|
+
const result: AutoImportRuleRunResult = await runRule(isDryRun);
|
|
2636
|
+
|
|
2637
|
+
expect(result).toMatchObject({
|
|
2638
|
+
hostsMatched: 3,
|
|
2639
|
+
hostsSkippedAlreadyRegistered: 2,
|
|
2640
|
+
devicesCreated: isDryRun ? 0 : 1,
|
|
2641
|
+
monitorsCreated: isDryRun ? 0 : 1,
|
|
2642
|
+
monitorsWouldCreate: isDryRun ? 1 : 0,
|
|
2643
|
+
monitorsSkippedAlreadyExisting: 2,
|
|
2644
|
+
matchedIpAddressSample: ["10.0.0.5"],
|
|
2645
|
+
});
|
|
2646
|
+
expect(createMock).toHaveBeenCalledTimes(isDryRun ? 0 : 1);
|
|
2647
|
+
expect(monitorCreateMock).toHaveBeenCalledTimes(isDryRun ? 0 : 1);
|
|
2648
|
+
},
|
|
2649
|
+
);
|
|
2650
|
+
|
|
2651
|
+
it("still honors exclusions when the registered device has been deleted", async () => {
|
|
2652
|
+
ruleFindOneByMock.mockResolvedValue(makeRule());
|
|
2653
|
+
ruleFindByMock.mockResolvedValue([
|
|
2654
|
+
makeRule({
|
|
2655
|
+
id: ObjectID.generate(),
|
|
2656
|
+
isExclusion: true,
|
|
2657
|
+
ipMatchTarget: "10.0.0.5",
|
|
2658
|
+
}),
|
|
2659
|
+
]);
|
|
2660
|
+
mockRunNowScans([
|
|
2661
|
+
makeScan({
|
|
2662
|
+
discoveredDevices: [makeHost({ isAlreadyRegistered: true })],
|
|
2663
|
+
}),
|
|
2664
|
+
]);
|
|
2665
|
+
|
|
2666
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
2667
|
+
|
|
2668
|
+
expect(result).toMatchObject({
|
|
2669
|
+
hostsExcluded: 1,
|
|
2670
|
+
hostsMatched: 0,
|
|
2671
|
+
devicesCreated: 0,
|
|
2672
|
+
});
|
|
2673
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
2674
|
+
expect(scanUpdateMock).not.toHaveBeenCalled();
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
it("resumes re-importing a deleted 903-device fleet across the run cap without duplicates", async () => {
|
|
2678
|
+
const hosts: Array<DiscoveredNetworkDevice> = Array.from(
|
|
2679
|
+
{ length: 903 },
|
|
2680
|
+
(_value: unknown, index: number): DiscoveredNetworkDevice => {
|
|
2681
|
+
return makeHost({
|
|
2682
|
+
ipAddress: `10.0.${Math.floor(index / 256)}.${index % 256}`,
|
|
2683
|
+
sysName: `switch-${index}`,
|
|
2684
|
+
isAlreadyRegistered: true,
|
|
2685
|
+
});
|
|
2686
|
+
},
|
|
2687
|
+
);
|
|
2688
|
+
const scan: NetworkDeviceDiscoveryScan = makeScan({
|
|
2689
|
+
autoImportProcessedAt: OneUptimeDate.getCurrentDate(),
|
|
2690
|
+
discoveredDevices: hosts,
|
|
2691
|
+
});
|
|
2692
|
+
const inventory: Map<string, NetworkDevice> = new Map();
|
|
2693
|
+
devicesByHostnamesMock.mockImplementation(
|
|
2694
|
+
async (data: {
|
|
2695
|
+
hostnames: Array<string>;
|
|
2696
|
+
}): Promise<Map<string, NetworkDevice>> => {
|
|
2697
|
+
const found: Map<string, NetworkDevice> = new Map();
|
|
2698
|
+
for (const hostname of data.hostnames) {
|
|
2699
|
+
const device: NetworkDevice | undefined = inventory.get(hostname);
|
|
2700
|
+
if (device) {
|
|
2701
|
+
found.set(hostname, device);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
return found;
|
|
2705
|
+
},
|
|
2706
|
+
);
|
|
2707
|
+
createMock.mockImplementation(
|
|
2708
|
+
async ({ data }: { data: NetworkDevice }): Promise<NetworkDevice> => {
|
|
2709
|
+
data.id = ObjectID.generate();
|
|
2710
|
+
inventory.set(data.hostname!, data);
|
|
2711
|
+
return data;
|
|
2712
|
+
},
|
|
2713
|
+
);
|
|
2714
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
2715
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
2716
|
+
);
|
|
2717
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
2718
|
+
|
|
2719
|
+
mockRunNowScans([scan]);
|
|
2720
|
+
const firstRun: AutoImportRuleRunResult = await runRule(false);
|
|
2721
|
+
expect(firstRun).toMatchObject({
|
|
2722
|
+
devicesCreated: MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
2723
|
+
hostsSkippedAlreadyRegistered: 0,
|
|
2724
|
+
isTruncated: true,
|
|
2725
|
+
});
|
|
2726
|
+
|
|
2727
|
+
mockRunNowScans([scan]);
|
|
2728
|
+
const secondRun: AutoImportRuleRunResult = await runRule(false);
|
|
2729
|
+
expect(secondRun).toMatchObject({
|
|
2730
|
+
devicesCreated: hosts.length - MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
2731
|
+
hostsSkippedAlreadyRegistered: MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
2732
|
+
isTruncated: false,
|
|
2733
|
+
});
|
|
2734
|
+
|
|
2735
|
+
mockRunNowScans([scan]);
|
|
2736
|
+
const thirdRun: AutoImportRuleRunResult = await runRule(false);
|
|
2737
|
+
expect(thirdRun).toMatchObject({
|
|
2738
|
+
devicesCreated: 0,
|
|
2739
|
+
hostsSkippedAlreadyRegistered: hosts.length,
|
|
2740
|
+
isTruncated: false,
|
|
2741
|
+
});
|
|
2742
|
+
expect(inventory.size).toBe(hosts.length);
|
|
2743
|
+
expect(createMock).toHaveBeenCalledTimes(hosts.length);
|
|
2744
|
+
expect(scanUpdateMock).toHaveBeenCalledTimes(2);
|
|
2745
|
+
});
|
|
2746
|
+
});
|
|
2747
|
+
|
|
2065
2748
|
/*
|
|
2066
2749
|
* github.com/OneUptime/oneuptime/issues/3599 — "Auto Import Rule can't see
|
|
2067
2750
|
* already-discovered devices while the Discovery Scan is still running".
|
|
@@ -2250,3 +2933,372 @@ describe("auto-import from a scan that is still sweeping (issue #3599)", () => {
|
|
|
2250
2933
|
]);
|
|
2251
2934
|
});
|
|
2252
2935
|
});
|
|
2936
|
+
|
|
2937
|
+
/*
|
|
2938
|
+
* OneUptime issue #3642: a discovery scan found 909 routers, the operator ran
|
|
2939
|
+
* the auto-import rule, "500+" arrived, and the remaining 400-odd were
|
|
2940
|
+
* "simply missing, with no error shown".
|
|
2941
|
+
*
|
|
2942
|
+
* The per-run caps were doing their job. What made them read as silence is
|
|
2943
|
+
* that a capped run used to STOP EVALUATING at the cap, so every number it
|
|
2944
|
+
* reported described only the part of the estate it happened to reach: 909
|
|
2945
|
+
* became "matched 500 out of the 500 discovered hosts it looked at", and
|
|
2946
|
+
* nothing in the answer said how many were left or that the monitor half of
|
|
2947
|
+
* the work has its own separate ceiling.
|
|
2948
|
+
*
|
|
2949
|
+
* These pin the fix at the engine: writes stop at the cap, counting does not.
|
|
2950
|
+
*/
|
|
2951
|
+
describe("a capped run reports the remainder (issue #3642)", () => {
|
|
2952
|
+
// A fleet whose hosts all match, addressed so no two collide.
|
|
2953
|
+
function makeFleet(size: number): Array<DiscoveredNetworkDevice> {
|
|
2954
|
+
return Array.from(
|
|
2955
|
+
{ length: size },
|
|
2956
|
+
(_value: unknown, index: number): DiscoveredNetworkDevice => {
|
|
2957
|
+
return makeHost({
|
|
2958
|
+
ipAddress: `10.5.${Math.floor(index / 256)}.${index % 256}`,
|
|
2959
|
+
sysName: `WANRTR-${index}`,
|
|
2960
|
+
});
|
|
2961
|
+
},
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
function makeInventoryFor(
|
|
2966
|
+
hosts: Array<DiscoveredNetworkDevice>,
|
|
2967
|
+
): Array<NetworkDevice> {
|
|
2968
|
+
return hosts.map((host: DiscoveredNetworkDevice): NetworkDevice => {
|
|
2969
|
+
return makeExistingDevice({
|
|
2970
|
+
id: ObjectID.generate(),
|
|
2971
|
+
hostname: host.ipAddress,
|
|
2972
|
+
name: host.sysName,
|
|
2973
|
+
});
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
it("counts the hosts the device cap left un-imported, and still evaluates them all", async () => {
|
|
2978
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeFleet(909);
|
|
2979
|
+
|
|
2980
|
+
scanFindOneByMock.mockResolvedValue(makeScan({ discoveredDevices: hosts }));
|
|
2981
|
+
ruleFindByMock.mockResolvedValue([
|
|
2982
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
2983
|
+
]);
|
|
2984
|
+
|
|
2985
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
2986
|
+
|
|
2987
|
+
// Writes stop at the cap…
|
|
2988
|
+
expect(createMock).toHaveBeenCalledTimes(MAX_DEVICES_PER_AUTO_IMPORT_RUN);
|
|
2989
|
+
expect(result).toMatchObject({
|
|
2990
|
+
devicesCreated: MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
2991
|
+
isTruncated: true,
|
|
2992
|
+
// …counting does not: the whole scan is accounted for.
|
|
2993
|
+
hostsEvaluated: 909,
|
|
2994
|
+
hostsMatched: 909,
|
|
2995
|
+
hostsPendingImport: 909 - MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
2996
|
+
monitorsPendingCreation: 0,
|
|
2997
|
+
});
|
|
2998
|
+
});
|
|
2999
|
+
|
|
3000
|
+
/*
|
|
3001
|
+
* The reporter's own shape: 909 devices already in the inventory, zero
|
|
3002
|
+
* monitors, and a rule that has since had a Monitor Template attached. No
|
|
3003
|
+
* device is created at all, so the DEVICE cap is never even approached —
|
|
3004
|
+
* only the separate monitor cap stops the run.
|
|
3005
|
+
*/
|
|
3006
|
+
it("counts the devices the monitor cap left unmonitored", async () => {
|
|
3007
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeFleet(909);
|
|
3008
|
+
|
|
3009
|
+
scanFindOneByMock.mockResolvedValue(makeScan({ discoveredDevices: hosts }));
|
|
3010
|
+
deviceFindByMock.mockResolvedValue(makeInventoryFor(hosts));
|
|
3011
|
+
ruleFindByMock.mockResolvedValue([
|
|
3012
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8", monitorTemplateId: TEMPLATE_ID }),
|
|
3013
|
+
]);
|
|
3014
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
3015
|
+
|
|
3016
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
3017
|
+
|
|
3018
|
+
expect(monitorCreateMock).toHaveBeenCalledTimes(
|
|
3019
|
+
MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
3020
|
+
);
|
|
3021
|
+
expect(result).toMatchObject({
|
|
3022
|
+
devicesCreated: 0,
|
|
3023
|
+
monitorsCreated: MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
3024
|
+
isTruncated: true,
|
|
3025
|
+
hostsEvaluated: 909,
|
|
3026
|
+
hostsMatched: 909,
|
|
3027
|
+
hostsSkippedAlreadyRegistered: 909,
|
|
3028
|
+
// The number the report was missing.
|
|
3029
|
+
monitorsPendingCreation: 909 - MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
3030
|
+
hostsPendingImport: 0,
|
|
3031
|
+
});
|
|
3032
|
+
});
|
|
3033
|
+
|
|
3034
|
+
/*
|
|
3035
|
+
* A device cap reached first must not be reported as a monitor shortfall:
|
|
3036
|
+
* the hosts past it have no device yet, so what they are waiting on is the
|
|
3037
|
+
* import, not a monitor.
|
|
3038
|
+
*/
|
|
3039
|
+
it("does not double-count un-imported hosts as unmonitored devices", async () => {
|
|
3040
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeFleet(600);
|
|
3041
|
+
|
|
3042
|
+
scanFindOneByMock.mockResolvedValue(makeScan({ discoveredDevices: hosts }));
|
|
3043
|
+
ruleFindByMock.mockResolvedValue([
|
|
3044
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8", monitorTemplateId: TEMPLATE_ID }),
|
|
3045
|
+
]);
|
|
3046
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
3047
|
+
|
|
3048
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
3049
|
+
|
|
3050
|
+
expect(result).toMatchObject({
|
|
3051
|
+
devicesCreated: MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
3052
|
+
hostsPendingImport: 600 - MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
3053
|
+
monitorsPendingCreation: 0,
|
|
3054
|
+
isTruncated: true,
|
|
3055
|
+
});
|
|
3056
|
+
});
|
|
3057
|
+
|
|
3058
|
+
/*
|
|
3059
|
+
* Where the issue actually lands for an operator: pressing "Run Rule"
|
|
3060
|
+
* twice must finish the 909, and the second press must report nothing
|
|
3061
|
+
* left. (The dashboard now chains these passes itself — see
|
|
3062
|
+
* AutoImportRunChain — so this is the server half of one button press.)
|
|
3063
|
+
*/
|
|
3064
|
+
it("finishes the 909-device monitor backfill over two manual runs", async () => {
|
|
3065
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeFleet(909);
|
|
3066
|
+
const devices: Array<NetworkDevice> = makeInventoryFor(hosts);
|
|
3067
|
+
const scan: NetworkDeviceDiscoveryScan = makeScan({
|
|
3068
|
+
discoveredDevices: hosts,
|
|
3069
|
+
});
|
|
3070
|
+
|
|
3071
|
+
/*
|
|
3072
|
+
* Monitors accumulate across the two runs the way the database would:
|
|
3073
|
+
* the second run's project-wide snapshot sees what the first created,
|
|
3074
|
+
* and its provenance keys are what make the resume idempotent.
|
|
3075
|
+
*/
|
|
3076
|
+
const provisioned: Array<Monitor> = [];
|
|
3077
|
+
monitorCreateMock.mockImplementation(
|
|
3078
|
+
({ data }: { data: Monitor }): Promise<Monitor> => {
|
|
3079
|
+
data.id = data.id || ObjectID.generate();
|
|
3080
|
+
provisioned.push(data);
|
|
3081
|
+
return Promise.resolve(data);
|
|
3082
|
+
},
|
|
3083
|
+
);
|
|
3084
|
+
monitorFindByMock.mockImplementation((): Promise<Array<Monitor>> => {
|
|
3085
|
+
return Promise.resolve(provisioned);
|
|
3086
|
+
});
|
|
3087
|
+
|
|
3088
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
3089
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8", monitorTemplateId: TEMPLATE_ID }),
|
|
3090
|
+
);
|
|
3091
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
3092
|
+
deviceFindByMock.mockResolvedValue(devices);
|
|
3093
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
3094
|
+
|
|
3095
|
+
mockRunNowScans([scan]);
|
|
3096
|
+
const firstRun: AutoImportRuleRunResult = await runRule(false);
|
|
3097
|
+
|
|
3098
|
+
expect(firstRun).toMatchObject({
|
|
3099
|
+
monitorsCreated: MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
3100
|
+
monitorsPendingCreation: 909 - MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
3101
|
+
isTruncated: true,
|
|
3102
|
+
});
|
|
3103
|
+
|
|
3104
|
+
mockRunNowScans([scan]);
|
|
3105
|
+
const secondRun: AutoImportRuleRunResult = await runRule(false);
|
|
3106
|
+
|
|
3107
|
+
expect(secondRun).toMatchObject({
|
|
3108
|
+
monitorsCreated: 909 - MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
3109
|
+
monitorsSkippedAlreadyExisting: MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
3110
|
+
monitorsPendingCreation: 0,
|
|
3111
|
+
isTruncated: false,
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
// Every router ends up monitored exactly once.
|
|
3115
|
+
expect(provisioned).toHaveLength(909);
|
|
3116
|
+
expect(
|
|
3117
|
+
new Set(
|
|
3118
|
+
provisioned.map((monitor: Monitor): string => {
|
|
3119
|
+
return monitor.autoProvisionedNetworkDeviceId!.toString();
|
|
3120
|
+
}),
|
|
3121
|
+
).size,
|
|
3122
|
+
).toBe(909);
|
|
3123
|
+
});
|
|
3124
|
+
|
|
3125
|
+
/*
|
|
3126
|
+
* A capped manual run stops OPENING scans — each is a multi-megabyte jsonb
|
|
3127
|
+
* read it could not act on anyway — so the remainder it counted is a floor.
|
|
3128
|
+
* Reporting it as a total would have the operator run again and watch more
|
|
3129
|
+
* than the promised number appear.
|
|
3130
|
+
*/
|
|
3131
|
+
it("says the remainder is a floor when a cap left scans unread", async () => {
|
|
3132
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeFleet(600);
|
|
3133
|
+
|
|
3134
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
3135
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
3136
|
+
);
|
|
3137
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
3138
|
+
mockRunNowScans([
|
|
3139
|
+
makeScan({ discoveredDevices: hosts }),
|
|
3140
|
+
makeScan({ discoveredDevices: makeFleet(5) }),
|
|
3141
|
+
]);
|
|
3142
|
+
|
|
3143
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
3144
|
+
|
|
3145
|
+
expect(result).toMatchObject({
|
|
3146
|
+
isTruncated: true,
|
|
3147
|
+
hostsPendingImport: 600 - MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
3148
|
+
hasUnevaluatedScans: true,
|
|
3149
|
+
});
|
|
3150
|
+
// The second scan was never opened.
|
|
3151
|
+
expect(scanFindOneByMock).toHaveBeenCalledTimes(1);
|
|
3152
|
+
});
|
|
3153
|
+
|
|
3154
|
+
it("reports an exact remainder when the capped scan was the last one", async () => {
|
|
3155
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
3156
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
3157
|
+
);
|
|
3158
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
3159
|
+
mockRunNowScans([makeScan({ discoveredDevices: makeFleet(600) })]);
|
|
3160
|
+
|
|
3161
|
+
const result: AutoImportRuleRunResult = await runRule(false);
|
|
3162
|
+
|
|
3163
|
+
expect(result).toMatchObject({
|
|
3164
|
+
isTruncated: true,
|
|
3165
|
+
hostsPendingImport: 600 - MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
3166
|
+
hasUnevaluatedScans: false,
|
|
3167
|
+
});
|
|
3168
|
+
});
|
|
3169
|
+
|
|
3170
|
+
/*
|
|
3171
|
+
* The dry run predicts the real run, so it must predict the leftovers too
|
|
3172
|
+
* — otherwise "what would this rule import" answers with the cap instead
|
|
3173
|
+
* of with the estate.
|
|
3174
|
+
*/
|
|
3175
|
+
it("lets a dry run predict what a real run would leave over", async () => {
|
|
3176
|
+
ruleFindOneByMock.mockResolvedValue(
|
|
3177
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
3178
|
+
);
|
|
3179
|
+
ruleFindByMock.mockResolvedValue([]);
|
|
3180
|
+
mockRunNowScans([makeScan({ discoveredDevices: makeFleet(909) })]);
|
|
3181
|
+
|
|
3182
|
+
const result: AutoImportRuleRunResult = await runRule(true);
|
|
3183
|
+
|
|
3184
|
+
expect(result).toMatchObject({
|
|
3185
|
+
isDryRun: true,
|
|
3186
|
+
isTruncated: true,
|
|
3187
|
+
hostsEvaluated: 909,
|
|
3188
|
+
hostsMatched: 909,
|
|
3189
|
+
hostsPendingImport: 909 - MAX_DEVICES_PER_AUTO_IMPORT_RUN,
|
|
3190
|
+
});
|
|
3191
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
/*
|
|
3195
|
+
* Counting past the cap must not start issuing per-device queries again:
|
|
3196
|
+
* the unindexed monitor JSON search is exactly what the budget check
|
|
3197
|
+
* guards, and running it for every host past the cap would turn a capped
|
|
3198
|
+
* pass into the full-table walk the cap exists to avoid.
|
|
3199
|
+
*/
|
|
3200
|
+
it("issues no per-device monitor searches for the hosts it only counted", async () => {
|
|
3201
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeFleet(600);
|
|
3202
|
+
|
|
3203
|
+
scanFindOneByMock.mockResolvedValue(makeScan({ discoveredDevices: hosts }));
|
|
3204
|
+
deviceFindByMock.mockResolvedValue(makeInventoryFor(hosts));
|
|
3205
|
+
ruleFindByMock.mockResolvedValue([
|
|
3206
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8", monitorTemplateId: TEMPLATE_ID }),
|
|
3207
|
+
]);
|
|
3208
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
3209
|
+
|
|
3210
|
+
await processScan();
|
|
3211
|
+
|
|
3212
|
+
/*
|
|
3213
|
+
* One project-wide snapshot plus one race-check per device the run
|
|
3214
|
+
* actually attempted — and none for the 100 it merely counted.
|
|
3215
|
+
*/
|
|
3216
|
+
expect(monitorFindByMock).toHaveBeenCalledTimes(
|
|
3217
|
+
1 + MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
3218
|
+
);
|
|
3219
|
+
});
|
|
3220
|
+
|
|
3221
|
+
/*
|
|
3222
|
+
* A stored result lists duplicate rows often enough that the import path
|
|
3223
|
+
* has a bucket for them. Counting rows rather than distinct work would
|
|
3224
|
+
* inflate the very number this fix exists to make trustworthy.
|
|
3225
|
+
*/
|
|
3226
|
+
it("counts a duplicated un-imported host once", async () => {
|
|
3227
|
+
const overflow: DiscoveredNetworkDevice = makeHost({
|
|
3228
|
+
ipAddress: "10.9.9.9",
|
|
3229
|
+
sysName: "WANRTR-overflow",
|
|
3230
|
+
});
|
|
3231
|
+
const hosts: Array<DiscoveredNetworkDevice> = [
|
|
3232
|
+
...makeFleet(MAX_DEVICES_PER_AUTO_IMPORT_RUN),
|
|
3233
|
+
overflow,
|
|
3234
|
+
{ ...overflow },
|
|
3235
|
+
{ ...overflow },
|
|
3236
|
+
];
|
|
3237
|
+
|
|
3238
|
+
scanFindOneByMock.mockResolvedValue(makeScan({ discoveredDevices: hosts }));
|
|
3239
|
+
ruleFindByMock.mockResolvedValue([
|
|
3240
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
3241
|
+
]);
|
|
3242
|
+
|
|
3243
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
3244
|
+
|
|
3245
|
+
expect(result).toMatchObject({
|
|
3246
|
+
isTruncated: true,
|
|
3247
|
+
// Three rows, one address, one host still to import.
|
|
3248
|
+
hostsPendingImport: 1,
|
|
3249
|
+
hostsEvaluated: MAX_DEVICES_PER_AUTO_IMPORT_RUN + 3,
|
|
3250
|
+
});
|
|
3251
|
+
});
|
|
3252
|
+
|
|
3253
|
+
it("counts a duplicated unmonitored device once", async () => {
|
|
3254
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeFleet(
|
|
3255
|
+
MAX_MONITORS_PER_AUTO_IMPORT_RUN,
|
|
3256
|
+
);
|
|
3257
|
+
const overflow: DiscoveredNetworkDevice = makeHost({
|
|
3258
|
+
ipAddress: "10.9.9.9",
|
|
3259
|
+
sysName: "WANRTR-overflow",
|
|
3260
|
+
});
|
|
3261
|
+
const withDuplicates: Array<DiscoveredNetworkDevice> = [
|
|
3262
|
+
...hosts,
|
|
3263
|
+
overflow,
|
|
3264
|
+
{ ...overflow },
|
|
3265
|
+
];
|
|
3266
|
+
|
|
3267
|
+
scanFindOneByMock.mockResolvedValue(
|
|
3268
|
+
makeScan({ discoveredDevices: withDuplicates }),
|
|
3269
|
+
);
|
|
3270
|
+
deviceFindByMock.mockResolvedValue(makeInventoryFor(withDuplicates));
|
|
3271
|
+
ruleFindByMock.mockResolvedValue([
|
|
3272
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8", monitorTemplateId: TEMPLATE_ID }),
|
|
3273
|
+
]);
|
|
3274
|
+
monitorTemplateFindByMock.mockResolvedValue([makeTemplate()]);
|
|
3275
|
+
|
|
3276
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
3277
|
+
|
|
3278
|
+
expect(result).toMatchObject({
|
|
3279
|
+
isTruncated: true,
|
|
3280
|
+
monitorsPendingCreation: 1,
|
|
3281
|
+
});
|
|
3282
|
+
});
|
|
3283
|
+
|
|
3284
|
+
/*
|
|
3285
|
+
* The resume protocol is unchanged by the new accounting: a capped pass
|
|
3286
|
+
* that made progress still leaves the marker NULL so the next worker tick
|
|
3287
|
+
* continues, and the counting past the cap must not accidentally stamp it.
|
|
3288
|
+
*/
|
|
3289
|
+
it("still leaves a capped pass's marker unstamped so the sweep resumes", async () => {
|
|
3290
|
+
const hosts: Array<DiscoveredNetworkDevice> = makeFleet(600);
|
|
3291
|
+
|
|
3292
|
+
scanFindOneByMock.mockResolvedValue(makeScan({ discoveredDevices: hosts }));
|
|
3293
|
+
ruleFindByMock.mockResolvedValue([
|
|
3294
|
+
makeRule({ ipMatchTarget: "10.0.0.0/8" }),
|
|
3295
|
+
]);
|
|
3296
|
+
|
|
3297
|
+
await processScan();
|
|
3298
|
+
|
|
3299
|
+
expect(scanUpdateMock).toHaveBeenCalledTimes(1);
|
|
3300
|
+
const updateCall: { data: JSONObject } = scanUpdateMock.mock
|
|
3301
|
+
.calls[0]![0] as { data: JSONObject };
|
|
3302
|
+
expect(Object.keys(updateCall.data)).not.toContain("autoImportProcessedAt");
|
|
3303
|
+
});
|
|
3304
|
+
});
|