@oneuptime/common 12.0.9 → 12.0.11
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/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Models/DatabaseModels/RunnerJob.ts +17 -2
- package/Server/API/LlmProviderAPI.ts +8 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Services/NetworkDeviceService.ts +265 -12
- package/Server/Services/RunnerJobService.ts +24 -1
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Utils/LLM/LLMService.ts +108 -19
- package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
- package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
- package/Server/Utils/SSRFProtection.ts +5 -4
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
- package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
- package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
- package/Tests/Types/API/URLTelScheme.test.ts +261 -0
- package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
- package/Tests/UI/Components/Button.test.tsx +279 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
- package/Tests/UI/Components/Icon.test.tsx +57 -0
- package/Tests/UI/Components/List.test.tsx +20 -3
- package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
- package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/UI/Components/Pagination.test.tsx +721 -146
- package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
- package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
- package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
- package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
- package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/API/Protocol.ts +9 -0
- package/Types/API/URL.ts +286 -20
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
- package/Types/Runbook/RunbookStepType.ts +23 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
- package/UI/Components/Button/Button.tsx +14 -22
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/Dictionary/Dictionary.tsx +4 -39
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/UI/Components/Pagination/Pagination.tsx +373 -328
- package/UI/Components/Pagination/PaginationUtil.ts +263 -0
- package/UI/Components/Pagination/Screenshots/README.md +15 -0
- package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
- package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
- package/UI/Components/TextArea/TextArea.tsx +3 -0
- package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
- package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
- package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
- package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
- package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
- package/Utils/API.ts +243 -36
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +9 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.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/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +20 -2
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
- package/build/dist/Server/Utils/SSRFProtection.js +5 -4
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Types/API/Protocol.js +9 -0
- package/build/dist/Types/API/Protocol.js.map +1 -1
- package/build/dist/Types/API/URL.js +241 -18
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +10 -16
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
- package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
- package/build/dist/Utils/API.js +175 -34
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
- package/package.json +1 -1
|
@@ -2,24 +2,140 @@ import DatabaseService from "./DatabaseService";
|
|
|
2
2
|
import Model from "../../Models/DatabaseModels/NetworkDeviceLink";
|
|
3
3
|
import NetworkDeviceService from "./NetworkDeviceService";
|
|
4
4
|
import NetworkDevice from "../../Models/DatabaseModels/NetworkDevice";
|
|
5
|
-
import { OnCreate } from "../Types/Database/Hooks";
|
|
5
|
+
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
|
6
6
|
import CreateBy from "../Types/Database/CreateBy";
|
|
7
|
+
import UpdateBy from "../Types/Database/UpdateBy";
|
|
7
8
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
8
9
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
9
10
|
import ObjectID from "../../Types/ObjectID";
|
|
10
11
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
11
12
|
import RelationIdUtil from "../Utils/Database/RelationIdUtil";
|
|
12
13
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
14
|
+
import Query from "../Types/Database/Query";
|
|
15
|
+
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
13
16
|
|
|
14
17
|
// Both spellings of each end, for the same reason RelationIdUtil exists.
|
|
15
18
|
const FROM_DEVICE_KEYS: Array<string> = ["fromDeviceId", "fromDevice"];
|
|
16
19
|
const TO_DEVICE_KEYS: Array<string> = ["toDeviceId", "toDevice"];
|
|
20
|
+
const PARENT_DEVICE_KEYS: Array<string> = ["parentDeviceId", "parentDevice"];
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* A device id, compared the way Postgres compares it.
|
|
24
|
+
*
|
|
25
|
+
* These columns are `uuid`. Postgres normalises and matches them without
|
|
26
|
+
* regard to case, and ObjectID.isValidUUID accepts either case, so an API
|
|
27
|
+
* client may legitimately send `AAAAAAAA-...` for a row stored as
|
|
28
|
+
* `aaaaaaaa-...`. A raw JS !== between the two says "different device" and
|
|
29
|
+
* refuses a parent that is in fact an end of the link. Note that the
|
|
30
|
+
* device-existence check below cannot disagree: it runs in Postgres.
|
|
31
|
+
*/
|
|
32
|
+
const sameDeviceId: (a: ObjectID, b: ObjectID) => boolean = (
|
|
33
|
+
a: ObjectID,
|
|
34
|
+
b: ObjectID,
|
|
35
|
+
): boolean => {
|
|
36
|
+
return a.toString().toLowerCase() === b.toString().toLowerCase();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* A declared parent has to be one of the two devices the link connects.
|
|
41
|
+
* Anything else is not a statement about this link at all, and the map
|
|
42
|
+
* would silently ignore it — the operator would have declared a hierarchy
|
|
43
|
+
* and watched nothing change.
|
|
44
|
+
*/
|
|
45
|
+
const assertParentIsAnEnd: (
|
|
46
|
+
parentDeviceId: ObjectID | null,
|
|
47
|
+
fromDeviceId: ObjectID,
|
|
48
|
+
toDeviceId: ObjectID,
|
|
49
|
+
) => void = (
|
|
50
|
+
parentDeviceId: ObjectID | null,
|
|
51
|
+
fromDeviceId: ObjectID,
|
|
52
|
+
toDeviceId: ObjectID,
|
|
53
|
+
): void => {
|
|
54
|
+
if (!parentDeviceId) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
!sameDeviceId(parentDeviceId, fromDeviceId) &&
|
|
59
|
+
!sameDeviceId(parentDeviceId, toDeviceId)
|
|
60
|
+
) {
|
|
61
|
+
throw new BadDataException(
|
|
62
|
+
"The parent device must be one of the two devices this link connects.",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* Which of `keys` the payload actually WRITES.
|
|
69
|
+
*
|
|
70
|
+
* Not the same question as which keys are present. TypeORM drops
|
|
71
|
+
* undefined-valued keys before it builds the SET list — "it doesn't make
|
|
72
|
+
* sense to update undefined properties" — so a key holding undefined
|
|
73
|
+
* changes nothing in the database. Treating it as a write makes this hook
|
|
74
|
+
* validate a row that is never going to exist: model the parent as
|
|
75
|
+
* cleared, accept the write, and leave the stored parent stranded on a
|
|
76
|
+
* link whose end just moved. Null and "" are different and really do
|
|
77
|
+
* clear the column, which is why the test is against undefined alone.
|
|
78
|
+
*/
|
|
79
|
+
const writtenKeys: (
|
|
80
|
+
data: Record<string, unknown>,
|
|
81
|
+
keys: Array<string>,
|
|
82
|
+
) => Array<string> = (
|
|
83
|
+
data: Record<string, unknown>,
|
|
84
|
+
keys: Array<string>,
|
|
85
|
+
): Array<string> => {
|
|
86
|
+
return keys.filter((key: string) => {
|
|
87
|
+
return key in data && data[key] !== undefined;
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
* An update payload is a QueryDeepPartialEntity, so a column may hold a
|
|
93
|
+
* raw SQL-expression function instead of a value. RelationIdUtil reads one
|
|
94
|
+
* as null — indistinguishable from "cleared" — and the whole invariant is
|
|
95
|
+
* then checked against a row that bears no relation to what will be
|
|
96
|
+
* written. Nothing in the tree writes this model that way today; refusing
|
|
97
|
+
* is what keeps that true, and a caller who needs it can resolve the id
|
|
98
|
+
* before calling rather than have it silently mis-validated.
|
|
99
|
+
*/
|
|
100
|
+
const assertNoSqlExpression: (
|
|
101
|
+
data: Record<string, unknown>,
|
|
102
|
+
keys: Array<string>,
|
|
103
|
+
) => void = (data: Record<string, unknown>, keys: Array<string>): void => {
|
|
104
|
+
for (const key of keys) {
|
|
105
|
+
if (typeof data[key] === "function") {
|
|
106
|
+
throw new BadDataException(
|
|
107
|
+
`${key} cannot be set to a raw SQL expression on a network device link — the link's ends and its declared parent have to be checked against each other, which needs actual ids.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
17
112
|
|
|
18
113
|
export class Service extends DatabaseService<Model> {
|
|
19
114
|
public constructor() {
|
|
20
115
|
super(Model);
|
|
21
116
|
}
|
|
22
117
|
|
|
118
|
+
/*
|
|
119
|
+
* onBeforeUpdate runs before DatabaseService permission-checks the query,
|
|
120
|
+
* so reading the raw client query as root would hand the hook rows from
|
|
121
|
+
* other projects. Re-apply the caller's tenant here. (Same helper as
|
|
122
|
+
* NetworkDeviceService and NetworkSiteService — private per service,
|
|
123
|
+
* which is the shape the codebase already settled on.)
|
|
124
|
+
*/
|
|
125
|
+
private scopeQueryToCallerTenant(
|
|
126
|
+
query: Query<Model>,
|
|
127
|
+
props: DatabaseCommonInteractionProps,
|
|
128
|
+
): Query<Model> {
|
|
129
|
+
if (props.isRoot || !props.tenantId) {
|
|
130
|
+
return query;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...query,
|
|
135
|
+
projectId: props.tenantId,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
23
139
|
/*
|
|
24
140
|
* Two guards, both about links that would be nonsense on the map:
|
|
25
141
|
*
|
|
@@ -39,6 +155,12 @@ export class Service extends DatabaseService<Model> {
|
|
|
39
155
|
unknown
|
|
40
156
|
>;
|
|
41
157
|
|
|
158
|
+
assertNoSqlExpression(data, [
|
|
159
|
+
...FROM_DEVICE_KEYS,
|
|
160
|
+
...TO_DEVICE_KEYS,
|
|
161
|
+
...PARENT_DEVICE_KEYS,
|
|
162
|
+
]);
|
|
163
|
+
|
|
42
164
|
const fromDeviceId: ObjectID | null = RelationIdUtil.read(
|
|
43
165
|
data,
|
|
44
166
|
FROM_DEVICE_KEYS,
|
|
@@ -52,7 +174,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
52
174
|
throw new BadDataException("A link needs a device at each end.");
|
|
53
175
|
}
|
|
54
176
|
|
|
55
|
-
if (fromDeviceId
|
|
177
|
+
if (sameDeviceId(fromDeviceId, toDeviceId)) {
|
|
56
178
|
throw new BadDataException("A device cannot be linked to itself.");
|
|
57
179
|
}
|
|
58
180
|
|
|
@@ -77,8 +199,222 @@ export class Service extends DatabaseService<Model> {
|
|
|
77
199
|
);
|
|
78
200
|
}
|
|
79
201
|
|
|
202
|
+
assertParentIsAnEnd(
|
|
203
|
+
RelationIdUtil.read(data, PARENT_DEVICE_KEYS),
|
|
204
|
+
fromDeviceId,
|
|
205
|
+
toDeviceId,
|
|
206
|
+
);
|
|
207
|
+
|
|
80
208
|
return { createBy, carryForward: null };
|
|
81
209
|
}
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
* The same invariant, on the other write path.
|
|
213
|
+
*
|
|
214
|
+
* An update can break it two ways, and only one of them is obvious. The
|
|
215
|
+
* obvious one is setting a parent that is not on the link. The other is
|
|
216
|
+
* moving an END: re-point `toDevice` at a different switch and a parent
|
|
217
|
+
* that was valid a moment ago now names a device the link no longer
|
|
218
|
+
* touches. So the post-update state is what gets checked, assembled from
|
|
219
|
+
* the payload where it says something and from the stored row where it
|
|
220
|
+
* does not.
|
|
221
|
+
*
|
|
222
|
+
* An update matches a QUERY, not one row, and each matched row may have
|
|
223
|
+
* different ends — so every one of them is validated, and one bad row
|
|
224
|
+
* rejects the whole write. That is the conservative direction: a partial
|
|
225
|
+
* update that fixed some rows and left others contradicting themselves
|
|
226
|
+
* would be far harder to notice than a refusal.
|
|
227
|
+
*
|
|
228
|
+
* It also re-runs onBeforeCreate's TENANCY check on any end that moves.
|
|
229
|
+
* That check used to exist only on create, which left the update path
|
|
230
|
+
* able to re-point an end at another project's device — the exact threat
|
|
231
|
+
* onBeforeCreate's own comment describes, since the FK is global and the
|
|
232
|
+
* column is member-writable.
|
|
233
|
+
*
|
|
234
|
+
* Only costs a read when the payload touches an end or the parent at all;
|
|
235
|
+
* an update of ports or the bound monitor takes the early return.
|
|
236
|
+
*/
|
|
237
|
+
@CaptureSpan()
|
|
238
|
+
protected override async onBeforeUpdate(
|
|
239
|
+
updateBy: UpdateBy<Model>,
|
|
240
|
+
): Promise<OnUpdate<Model>> {
|
|
241
|
+
const data: Record<string, unknown> = (updateBy.data ||
|
|
242
|
+
{}) as unknown as Record<string, unknown>;
|
|
243
|
+
|
|
244
|
+
const hierarchyKeys: Array<string> = [
|
|
245
|
+
...FROM_DEVICE_KEYS,
|
|
246
|
+
...TO_DEVICE_KEYS,
|
|
247
|
+
...PARENT_DEVICE_KEYS,
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
if (writtenKeys(data, hierarchyKeys).length === 0) {
|
|
251
|
+
return { updateBy, carryForward: null };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
assertNoSqlExpression(data, hierarchyKeys);
|
|
255
|
+
|
|
256
|
+
const writtenFromDeviceId: ObjectID | null = RelationIdUtil.read(
|
|
257
|
+
data,
|
|
258
|
+
FROM_DEVICE_KEYS,
|
|
259
|
+
);
|
|
260
|
+
const writtenToDeviceId: ObjectID | null = RelationIdUtil.read(
|
|
261
|
+
data,
|
|
262
|
+
TO_DEVICE_KEYS,
|
|
263
|
+
);
|
|
264
|
+
const writtenParentDeviceId: ObjectID | null = RelationIdUtil.read(
|
|
265
|
+
data,
|
|
266
|
+
PARENT_DEVICE_KEYS,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* Writing the parent key with an empty value is how a hierarchy is
|
|
271
|
+
* cleared back to a peer link. RelationIdUtil reads that as null, which
|
|
272
|
+
* is indistinguishable from "not written" — so the presence of the key
|
|
273
|
+
* (with a value that will actually be written; see writtenKeys), not
|
|
274
|
+
* the truthiness of its value, is what decides whether the stored
|
|
275
|
+
* parent still applies.
|
|
276
|
+
*/
|
|
277
|
+
const isParentWritten: boolean =
|
|
278
|
+
writtenKeys(data, PARENT_DEVICE_KEYS).length > 0;
|
|
279
|
+
|
|
280
|
+
const existingLinks: Array<Model> = await this.findBy({
|
|
281
|
+
query: this.scopeQueryToCallerTenant(updateBy.query, updateBy.props),
|
|
282
|
+
select: {
|
|
283
|
+
_id: true,
|
|
284
|
+
projectId: true,
|
|
285
|
+
fromDeviceId: true,
|
|
286
|
+
toDeviceId: true,
|
|
287
|
+
parentDeviceId: true,
|
|
288
|
+
},
|
|
289
|
+
limit: LIMIT_MAX,
|
|
290
|
+
skip: 0,
|
|
291
|
+
props: {
|
|
292
|
+
isRoot: true,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
/*
|
|
297
|
+
* An end that MOVED has to be re-checked for tenancy, exactly as
|
|
298
|
+
* onBeforeCreate checks it. The FK only requires the device row to
|
|
299
|
+
* exist — it is global — so without this a project member could
|
|
300
|
+
* re-point an end of their own link at another project's device and
|
|
301
|
+
* read its name straight off the map, which is the threat
|
|
302
|
+
* onBeforeCreate's own comment describes. The declared parent inherits
|
|
303
|
+
* the check for free: it has to be one of the two ends.
|
|
304
|
+
*
|
|
305
|
+
* Grouped by project because an update may in principle match links in
|
|
306
|
+
* more than one, and a device is only legitimate within its own.
|
|
307
|
+
*/
|
|
308
|
+
/*
|
|
309
|
+
* Deduped: writing the same id to both ends is a self-link, and that
|
|
310
|
+
* has its own error below — it must not surface as "device not found".
|
|
311
|
+
*/
|
|
312
|
+
const movedEndIds: Array<string> = [
|
|
313
|
+
...new Set<string>(
|
|
314
|
+
[writtenFromDeviceId, writtenToDeviceId]
|
|
315
|
+
.filter((id: ObjectID | null): id is ObjectID => {
|
|
316
|
+
return Boolean(id);
|
|
317
|
+
})
|
|
318
|
+
.map((id: ObjectID) => {
|
|
319
|
+
return id.toString();
|
|
320
|
+
}),
|
|
321
|
+
),
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
if (movedEndIds.length > 0 && existingLinks.length > 0) {
|
|
325
|
+
/*
|
|
326
|
+
* Every project this write touches: the caller's own tenant, plus
|
|
327
|
+
* the project of each row the query matched (a root caller has no
|
|
328
|
+
* tenant, and only root can match rows across more than one). The
|
|
329
|
+
* new end has to be legitimate in all of them.
|
|
330
|
+
*
|
|
331
|
+
* Guarded on there being matched rows at all: a query that matched
|
|
332
|
+
* nothing writes nothing, and there is no link for a foreign device
|
|
333
|
+
* to end up attached to.
|
|
334
|
+
*/
|
|
335
|
+
const projectIds: Set<string> = new Set<string>();
|
|
336
|
+
if (updateBy.props.tenantId) {
|
|
337
|
+
projectIds.add(updateBy.props.tenantId.toString());
|
|
338
|
+
}
|
|
339
|
+
for (const link of existingLinks) {
|
|
340
|
+
if (link.projectId) {
|
|
341
|
+
projectIds.add(link.projectId.toString());
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/*
|
|
346
|
+
* Fail closed. No project to check against and an end on the move
|
|
347
|
+
* is not a safe update to wave through — it is the one shape where
|
|
348
|
+
* a device from anywhere could be attached to this link.
|
|
349
|
+
*/
|
|
350
|
+
if (projectIds.size === 0) {
|
|
351
|
+
throw new BadDataException(
|
|
352
|
+
"Both devices must exist and belong to this project.",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const projectId of projectIds) {
|
|
357
|
+
const devices: Array<NetworkDevice> = await NetworkDeviceService.findBy(
|
|
358
|
+
{
|
|
359
|
+
query: {
|
|
360
|
+
_id: QueryHelper.any(movedEndIds),
|
|
361
|
+
projectId: new ObjectID(projectId),
|
|
362
|
+
},
|
|
363
|
+
select: { _id: true },
|
|
364
|
+
limit: LIMIT_MAX,
|
|
365
|
+
skip: 0,
|
|
366
|
+
props: { isRoot: true },
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
/*
|
|
371
|
+
* Compared as a SET of ids rather than as a count. A count says
|
|
372
|
+
* "as many rows came back as I asked about", which a duplicate row
|
|
373
|
+
* satisfies just as well as the right rows do — and this is the
|
|
374
|
+
* check standing between one project's map and another project's
|
|
375
|
+
* device names, so it should be answering the actual question.
|
|
376
|
+
*/
|
|
377
|
+
const foundIds: Set<string> = new Set<string>(
|
|
378
|
+
devices.map((device: NetworkDevice) => {
|
|
379
|
+
return (device.id || device._id || "").toString().toLowerCase();
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const isMissingAnEnd: boolean = movedEndIds.some((id: string) => {
|
|
384
|
+
return !foundIds.has(id.toLowerCase());
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if (isMissingAnEnd) {
|
|
388
|
+
throw new BadDataException(
|
|
389
|
+
"Both devices must exist and belong to this project.",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const link of existingLinks) {
|
|
396
|
+
const fromDeviceId: ObjectID | undefined =
|
|
397
|
+
writtenFromDeviceId || link.fromDeviceId;
|
|
398
|
+
const toDeviceId: ObjectID | undefined =
|
|
399
|
+
writtenToDeviceId || link.toDeviceId;
|
|
400
|
+
const parentDeviceId: ObjectID | null = isParentWritten
|
|
401
|
+
? writtenParentDeviceId
|
|
402
|
+
: link.parentDeviceId || null;
|
|
403
|
+
|
|
404
|
+
// A row mid-migration with a missing end has no invariant to check.
|
|
405
|
+
if (!fromDeviceId || !toDeviceId) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (sameDeviceId(fromDeviceId, toDeviceId)) {
|
|
410
|
+
throw new BadDataException("A device cannot be linked to itself.");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
assertParentIsAnEnd(parentDeviceId, fromDeviceId, toDeviceId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { updateBy, carryForward: null };
|
|
417
|
+
}
|
|
82
418
|
}
|
|
83
419
|
|
|
84
420
|
export default new Service();
|
|
@@ -14,12 +14,14 @@ import UpdateBy from "../Types/Database/UpdateBy";
|
|
|
14
14
|
import Query from "../Types/Database/Query";
|
|
15
15
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
16
16
|
import logger, { LogAttributes } from "../Utils/Logger";
|
|
17
|
-
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
17
|
+
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
18
|
+
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
18
19
|
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
19
20
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
20
21
|
import ObjectID from "../../Types/ObjectID";
|
|
21
22
|
import OneUptimeDate from "../../Types/Date";
|
|
22
23
|
import CidrMatchUtil from "../../Utils/NetworkSite/CidrMatchUtil";
|
|
24
|
+
import { SiteAssignmentRuleRunResult } from "../../Types/NetworkAutomation/RuleRunResult";
|
|
23
25
|
import { NetworkDeviceMonitoringMethodUtil } from "../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
|
|
24
26
|
import RelationIdUtil from "../Utils/Database/RelationIdUtil";
|
|
25
27
|
import { EntityManager } from "typeorm";
|
|
@@ -78,6 +80,39 @@ function readMonitorIdFromData(data: Record<string, unknown>): ObjectID | null {
|
|
|
78
80
|
return RelationIdUtil.read(data, MONITOR_KEYS);
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
/*
|
|
84
|
+
* How many devices a single manual "Run now" of an assignment rule walks.
|
|
85
|
+
* The automatic path only ever sees one device at a time, so this cap is the
|
|
86
|
+
* only thing standing between a rule run and a full-fleet table scan inside
|
|
87
|
+
* one HTTP request. Runs that hit it report isTruncated so the caller can say
|
|
88
|
+
* "run it again" rather than quietly leave half the estate unassigned.
|
|
89
|
+
*/
|
|
90
|
+
export const MAX_DEVICES_PER_RULE_RUN: number = LIMIT_PER_PROJECT;
|
|
91
|
+
|
|
92
|
+
// Devices are read in pages so a large estate never lands in memory at once.
|
|
93
|
+
const RULE_RUN_PAGE_SIZE: number = 1000;
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* The identity a rule is matched against. The hostname column holds an IP
|
|
97
|
+
* address or a DNS name depending on how the device got here, so it is passed
|
|
98
|
+
* as both — ipInCidr rejects non-IP strings safely. Mirrors the single-device
|
|
99
|
+
* path in applySiteAssignmentRulesToDevice; both must stay in step or a
|
|
100
|
+
* manual run would disagree with what discovery does.
|
|
101
|
+
*/
|
|
102
|
+
function toRuleMatchTarget(device: Model): {
|
|
103
|
+
ip: string | undefined;
|
|
104
|
+
hostname: string | undefined;
|
|
105
|
+
sysName: string | undefined;
|
|
106
|
+
name: string | undefined;
|
|
107
|
+
} {
|
|
108
|
+
return {
|
|
109
|
+
ip: device.hostname,
|
|
110
|
+
hostname: device.hostname,
|
|
111
|
+
sysName: device.sysName,
|
|
112
|
+
name: device.name,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
81
116
|
export class Service extends DatabaseService<Model> {
|
|
82
117
|
public constructor() {
|
|
83
118
|
super(Model);
|
|
@@ -616,20 +651,14 @@ export class Service extends DatabaseService<Model> {
|
|
|
616
651
|
}
|
|
617
652
|
|
|
618
653
|
/*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
* only ever saw `hostname` could not match anything a user recognises.
|
|
654
|
+
* `name` is matched on as well as `hostname` because a discovery import
|
|
655
|
+
* puts the responding IP in `hostname` and the device's real identity in
|
|
656
|
+
* `name`, so a hostname pattern that only ever saw `hostname` could not
|
|
657
|
+
* match anything a user recognises. See toRuleMatchTarget.
|
|
624
658
|
*/
|
|
625
659
|
const winner: NetworkSiteAssignmentRule | null = CidrMatchUtil.pickRule(
|
|
626
660
|
rules,
|
|
627
|
-
|
|
628
|
-
ip: device.hostname,
|
|
629
|
-
hostname: device.hostname,
|
|
630
|
-
sysName: device.sysName,
|
|
631
|
-
name: device.name,
|
|
632
|
-
},
|
|
661
|
+
toRuleMatchTarget(device),
|
|
633
662
|
);
|
|
634
663
|
|
|
635
664
|
if (!winner || !winner.siteId) {
|
|
@@ -654,6 +683,230 @@ export class Service extends DatabaseService<Model> {
|
|
|
654
683
|
});
|
|
655
684
|
}
|
|
656
685
|
|
|
686
|
+
/*
|
|
687
|
+
* Runs ONE assignment rule against the devices that already exist, which is
|
|
688
|
+
* the half the automatic path cannot do: rules only ever fire on create, on
|
|
689
|
+
* an identity change, or on the next poll of a device that has no site, so
|
|
690
|
+
* a rule written after the estate was imported would never reach it
|
|
691
|
+
* (OneUptime/oneuptime#3191).
|
|
692
|
+
*
|
|
693
|
+
* Two rules of the automatic path are kept deliberately:
|
|
694
|
+
*
|
|
695
|
+
* - priority still decides. A device this rule matches but a
|
|
696
|
+
* higher-priority rule also matches is REPORTED, not moved: running one
|
|
697
|
+
* rule must never quietly do another rule's work, or the button would
|
|
698
|
+
* produce a placement no rule alone explains.
|
|
699
|
+
* - a device already in a site is left alone unless the caller explicitly
|
|
700
|
+
* asks otherwise. Nothing records whether a site was chosen by a human
|
|
701
|
+
* or by a rule, so overwriting is the caller's decision to make, with
|
|
702
|
+
* the warning that goes with it — and it is not needed for the case the
|
|
703
|
+
* button exists for, since a device imported before the rule existed
|
|
704
|
+
* has no site at all.
|
|
705
|
+
*/
|
|
706
|
+
@CaptureSpan()
|
|
707
|
+
public async applySiteAssignmentRuleToExistingDevices(data: {
|
|
708
|
+
ruleId: ObjectID;
|
|
709
|
+
projectId: ObjectID;
|
|
710
|
+
reassignDevicesAlreadyInASite: boolean;
|
|
711
|
+
}): Promise<SiteAssignmentRuleRunResult> {
|
|
712
|
+
/*
|
|
713
|
+
* Every rule in the project, not just the one being run — pickRule needs
|
|
714
|
+
* the whole set to answer "does something outrank this rule here?".
|
|
715
|
+
* Loading them by project is also what scopes the run to the tenant: a
|
|
716
|
+
* rule id from another project simply is not in this list.
|
|
717
|
+
*/
|
|
718
|
+
const rules: Array<NetworkSiteAssignmentRule> =
|
|
719
|
+
await NetworkSiteAssignmentRuleService.findBy({
|
|
720
|
+
query: {
|
|
721
|
+
projectId: data.projectId,
|
|
722
|
+
},
|
|
723
|
+
select: {
|
|
724
|
+
_id: true,
|
|
725
|
+
siteId: true,
|
|
726
|
+
subnetCidr: true,
|
|
727
|
+
hostnamePattern: true,
|
|
728
|
+
priority: true,
|
|
729
|
+
createdAt: true,
|
|
730
|
+
},
|
|
731
|
+
limit: LIMIT_MAX,
|
|
732
|
+
skip: 0,
|
|
733
|
+
props: {
|
|
734
|
+
isRoot: true,
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const rule: NetworkSiteAssignmentRule | undefined = rules.find(
|
|
739
|
+
(candidate: NetworkSiteAssignmentRule) => {
|
|
740
|
+
return candidate._id?.toString() === data.ruleId.toString();
|
|
741
|
+
},
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
if (!rule) {
|
|
745
|
+
throw new BadDataException("Assignment rule not found.");
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (!rule.siteId) {
|
|
749
|
+
throw new BadDataException(
|
|
750
|
+
"This assignment rule has no site to assign devices to.",
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const ruleSiteId: ObjectID = rule.siteId;
|
|
755
|
+
|
|
756
|
+
const result: SiteAssignmentRuleRunResult = {
|
|
757
|
+
devicesEvaluated: 0,
|
|
758
|
+
devicesMatched: 0,
|
|
759
|
+
devicesAssigned: 0,
|
|
760
|
+
devicesAlreadyInRuleSite: 0,
|
|
761
|
+
devicesSkippedAlreadyInAnotherSite: 0,
|
|
762
|
+
devicesClaimedByHigherPriorityRule: 0,
|
|
763
|
+
devicesFailed: 0,
|
|
764
|
+
isTruncated: false,
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
let skip: number = 0;
|
|
768
|
+
|
|
769
|
+
for (;;) {
|
|
770
|
+
const devices: Array<Model> = await this.findBy({
|
|
771
|
+
query: {
|
|
772
|
+
projectId: data.projectId,
|
|
773
|
+
},
|
|
774
|
+
select: {
|
|
775
|
+
_id: true,
|
|
776
|
+
siteId: true,
|
|
777
|
+
hostname: true,
|
|
778
|
+
sysName: true,
|
|
779
|
+
name: true,
|
|
780
|
+
},
|
|
781
|
+
/*
|
|
782
|
+
* Sorted by id so paging stays stable while the run writes to the
|
|
783
|
+
* very rows it is paging over. Assigning a site never changes an id,
|
|
784
|
+
* so no device can be skipped or seen twice.
|
|
785
|
+
*/
|
|
786
|
+
sort: {
|
|
787
|
+
_id: SortOrder.Ascending,
|
|
788
|
+
},
|
|
789
|
+
limit: RULE_RUN_PAGE_SIZE,
|
|
790
|
+
skip: skip,
|
|
791
|
+
props: {
|
|
792
|
+
isRoot: true,
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
if (devices.length === 0) {
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
for (const device of devices) {
|
|
801
|
+
result.devicesEvaluated++;
|
|
802
|
+
|
|
803
|
+
const target: ReturnType<typeof toRuleMatchTarget> =
|
|
804
|
+
toRuleMatchTarget(device);
|
|
805
|
+
|
|
806
|
+
if (!CidrMatchUtil.ruleMatches(rule, target)) {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
result.devicesMatched++;
|
|
811
|
+
|
|
812
|
+
if (device.siteId?.toString() === ruleSiteId.toString()) {
|
|
813
|
+
result.devicesAlreadyInRuleSite++;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (device.siteId && !data.reassignDevicesAlreadyInASite) {
|
|
818
|
+
result.devicesSkippedAlreadyInAnotherSite++;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const winner: NetworkSiteAssignmentRule | null = CidrMatchUtil.pickRule(
|
|
823
|
+
rules,
|
|
824
|
+
target,
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
if (winner?._id?.toString() !== rule._id?.toString()) {
|
|
828
|
+
result.devicesClaimedByHigherPriorityRule++;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/*
|
|
833
|
+
* Counted as a failure rather than skipped silently: every matched
|
|
834
|
+
* device has to land in exactly one bucket, or the summary the
|
|
835
|
+
* operator reads would not add up.
|
|
836
|
+
*/
|
|
837
|
+
if (!device.id) {
|
|
838
|
+
result.devicesFailed++;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/*
|
|
843
|
+
* Through updateOneById, not a bulk UPDATE: onUpdateSuccess is what
|
|
844
|
+
* refreshes the rollups of the site the device left and the site it
|
|
845
|
+
* joined, and a raw write would leave both stale.
|
|
846
|
+
*/
|
|
847
|
+
try {
|
|
848
|
+
await this.updateOneById({
|
|
849
|
+
id: device.id,
|
|
850
|
+
data: {
|
|
851
|
+
siteId: ruleSiteId,
|
|
852
|
+
},
|
|
853
|
+
props: {
|
|
854
|
+
isRoot: true,
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
result.devicesAssigned++;
|
|
859
|
+
} catch (error) {
|
|
860
|
+
// One unhappy device must not abandon the rest of the estate.
|
|
861
|
+
result.devicesFailed++;
|
|
862
|
+
logger.error(
|
|
863
|
+
`Error assigning site from assignment rule ${data.ruleId.toString()} to device ${device.id.toString()}: ${error}`,
|
|
864
|
+
{
|
|
865
|
+
projectId: data.projectId.toString(),
|
|
866
|
+
networkDeviceId: device.id.toString(),
|
|
867
|
+
} as LogAttributes,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
skip += devices.length;
|
|
873
|
+
|
|
874
|
+
if (devices.length < RULE_RUN_PAGE_SIZE) {
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (skip >= MAX_DEVICES_PER_RULE_RUN) {
|
|
879
|
+
/*
|
|
880
|
+
* A full last page does not prove there is an eleventh thousand of
|
|
881
|
+
* devices — an estate of exactly the cap would report a truncation
|
|
882
|
+
* that never happened, and the UI would tell the user to run it
|
|
883
|
+
* again forever. One row is enough to settle it.
|
|
884
|
+
*/
|
|
885
|
+
const nextDevice: Array<Model> = await this.findBy({
|
|
886
|
+
query: {
|
|
887
|
+
projectId: data.projectId,
|
|
888
|
+
},
|
|
889
|
+
select: {
|
|
890
|
+
_id: true,
|
|
891
|
+
},
|
|
892
|
+
sort: {
|
|
893
|
+
_id: SortOrder.Ascending,
|
|
894
|
+
},
|
|
895
|
+
limit: 1,
|
|
896
|
+
skip: skip,
|
|
897
|
+
props: {
|
|
898
|
+
isRoot: true,
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
result.isTruncated = nextDevice.length > 0;
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return result;
|
|
908
|
+
}
|
|
909
|
+
|
|
657
910
|
/*
|
|
658
911
|
* Atomically claims the devices this probe should poll now and advances
|
|
659
912
|
* each device's nextPollAt by its own polling interval — the device-owned
|