Skip to content

Commit 9f0e157

Browse files
authored
Merge pull request #45 from zeronetworks/pin-assets-to-cluster-by-subnet
Adding subnet support for Pin-AssetsToClusters.ps1.
2 parents 7a54c96 + 0f09ca7 commit 9f0e157

5 files changed

Lines changed: 741 additions & 221 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
*.csv
2+
*-DeploymentClusters.json
23
.env
34
.env.ps1
45
AssetDetailsFieldMappings.json
6+
DeploymentClusterFieldMappings.json
7+
subnet-pinning.md
8+
plan.md
9+
test-plan.md
10+
ZeroNetworksApi.yaml
11+
testing/
12+
*-DeploymentClusters.json
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
This is a standalone PowerShell 7+ utility, `Pin-AssetsToClusters.ps1`, that pins/unpins Zero Networks Segment assets to deployment clusters via the Zero Networks REST API (`/api/v1`). It is one script in a much larger multi-project monorepo (`Community`); treat this directory as its own self-contained unit — there is no shared build system, package manifest, or test runner across the monorepo.
8+
9+
There are no automated tests, linter config, or build step in this project. Validation is done by running the script directly (see below).
10+
11+
## Running the script
12+
13+
```powershell
14+
# List deployment clusters (also initializes cluster/segment-server lookups, and refreshes the name cache file)
15+
./Pin-AssetsToClusters.ps1 -ApiKey <key> -PortalUrl https://<tenant>-admin.zeronetworks.com -ListDeploymentClusters
16+
17+
# Export a CSV template for bulk operations
18+
./Pin-AssetsToClusters.ps1 -ExportCsvTemplate
19+
20+
# Pin/unpin a single asset (-DryRun previews without calling the mutation API)
21+
./Pin-AssetsToClusters.ps1 -ApiKey <key> -AssetId <assetId> -DeploymentClusterName <clusterName> [-Unpin] [-DryRun] [-EnableDebug]
22+
23+
# Bulk via CSV or AD OU path
24+
./Pin-AssetsToClusters.ps1 -ApiKey <key> -CsvPath ./assets.csv [-Unpin] [-DryRun]
25+
./Pin-AssetsToClusters.ps1 -ApiKey <key> -OUPath "OU=Computers,DC=domain,DC=com" -DeploymentClusterName <clusterName> [-DisableNestedOuResolution] [-StopOnAssetValidationError]
26+
27+
# Bulk via IPv4 subnet (CIDR)
28+
./Pin-AssetsToClusters.ps1 -ApiKey <key> -TargetSubnet "10.200.200.0/24" -DeploymentClusterName <clusterName> [-Unpin] [-DryRun] [-StopOnAssetValidationError]
29+
```
30+
31+
`-EnableDebug` sets `$DebugPreference = "Continue"` for verbose API/flow tracing. There is no `-WhatIf`/Pester test suite — use `-DryRun` against a real (or test) tenant to validate changes before committing.
32+
33+
All parameter sets that target a cluster take `-DeploymentClusterName`/a CSV `DeploymentClusterName` column, not a raw cluster ID — `-DeploymentClusterId` was removed entirely. Names are resolved to IDs via a local `<envName>-DeploymentClusters.json` cache file kept next to the script (`$PSScriptRoot`), where `envName` is derived from the `-PortalUrl` host. See `Resolve-DeploymentClusterName` and `Initialize-DeploymentClusterCache` below.
34+
35+
## Architecture
36+
37+
Single-file script driven by a `[CmdletBinding]` parameter-set switch (`ByAssetId`, `ByOuPath`, `ByCsvPath`, `ByTargetSubnet`, `ListDeploymentClusters`, `ExportCsvTemplate`). The final `switch ($PSCmdlet.ParameterSetName)` block at the bottom of the file is the entry point — read it first to see how each mode wires the helper functions together.
38+
39+
Key flow shared by all mutating parameter sets:
40+
1. `Initialize-ApiContext` — sets script-scoped `$script:Headers` / `$script:ApiBaseUrl` from `-ApiKey` / `-PortalUrl`, then calls `Initialize-DeploymentClusterCache` to ensure the local name cache file exists (creates it from the API if missing; does not refresh an existing one).
41+
2. `Resolve-DeploymentClusterName``Invoke-ValidateDeploymentClusterId` — resolves the user-supplied `-DeploymentClusterName` to a cluster ID via the cache file (throws immediately, listing known names, on a cache miss), then validates that ID exists and has an online segment server via `Get-DeploymentClusters` (lazily populates `$script:DeploymentClusterHashtable` and `$script:SegmentServerHashtable`, keyed by cluster ID and segment-server asset ID respectively, for O(1) lookups used throughout validation). Everything downstream of this step (batching/pinning functions) still operates on the resolved cluster ID, not the name.
42+
3. Asset resolution differs per mode: `Get-AssetDetails` (single asset), `Get-OUInfoFromApi` + `Get-AssetsFromOU` (OU mode), `Get-CsvData` (CSV mode, normalizes CSV rows into the same shape as API asset objects so downstream code is mode-agnostic), or `Get-SubnetHostAddresses` + `Get-AssetsByHostAddresses` (subnet mode, expands a CIDR range into host addresses and resolves them to monitored assets via a `lastIpAddress` filter).
43+
4. `Test-AssetCanBePinned` / `Test-ValidateProvidedAssetsCanBePinned` — enforces the pin/unpin prerequisites (not a segment server, monitored by Segment Server, healthy, applicable, correct current pin state). Validation order matters — see comments in `Test-AssetCanBePinned`.
44+
5. `Invoke-BatchBasedClusterPinning``Set-AssetsToDeploymentCluster` — batches assets in groups of 50 and calls the `PUT /assets/actions/deployments-cluster` endpoint (or prints the would-be request body under `-DryRun`).
45+
46+
API plumbing: `Invoke-ApiRequest` (single request + status-code validation via `Test-ApiResponseStatusCode`) is wrapped by `Invoke-PaginatedApiRequest`, which transparently follows both cursor-based (`nextCursor`) and offset-based (`nextOffset`/`count`) pagination and merges `items` across pages.
47+
48+
`$script:DeploymentClusterFieldMappings` (near the top of the script) decodes numeric enum codes returned by the API (cluster strategy, deployment status/state, service IDs) into human-readable strings via `Invoke-DecodeDeploymentClusterIDFields`. Note: `DeploymentClusterFieldMappings.json` in this directory holds the same mapping data as a standalone reference file but is not read by the script — the script keeps its own inline copy in `$script:DeploymentClusterFieldMappings`. If one is updated, update the other to keep them in sync.
49+
50+
`deploymentsClusterSource` on an asset (values 0–6) is the key field driving pin-state validation — see the block comment above `$AssetIsPinnedDeploymentClusterSource` in `Test-AssetCanBePinned` for the meaning of each code.
51+
52+
**Deployment cluster name resolution:** `Get-DeploymentClusterCachePath` derives `$envName` from the `-PortalUrl` host (stripping a trailing `.zeronetworks.com`) and returns `$PSScriptRoot/<envName>-DeploymentClusters.json`. `Save-DeploymentClusterCache` writes a flat `name -> id` JSON map to that path from a `Get-DeploymentClusters` result. `Initialize-DeploymentClusterCache` (called from `Initialize-ApiContext`) creates the file only if it's missing — it does not refresh an existing file. `Resolve-DeploymentClusterName` reads the cache file and resolves a name to an ID, throwing (and listing known names) on a miss. The `ListDeploymentClusters` parameter set is the one exception that always overwrites the cache via `Save-DeploymentClusterCache` after its own `Get-DeploymentClusters` call, since it already fetches fresh data — this is the documented way to refresh the cache after clusters are added/renamed. The cache file (`*-DeploymentClusters.json`) is gitignored.
53+
54+
### Per-Parameter-Set Workflow Sequences
55+
56+
The bottom-of-file `switch ($PSCmdlet.ParameterSetName)` block dispatches to one of these sequences. Each is self-contained — read the relevant `case` directly for exact call order.
57+
58+
**`ByAssetId`** (single asset, no batching):
59+
1. `Initialize-ApiContext`
60+
2. `Resolve-DeploymentClusterName` resolves `-DeploymentClusterName` to a cluster ID (local var `$DeploymentClusterId`), then `Invoke-ValidateDeploymentClusterId` validates that ID exists (and has an online segment server, unless `-SkipSegmentServerValidation`).
61+
3. `Test-AssetCanBePinned` — validates the single asset directly (not via `Test-ValidateProvidedAssetsCanBePinned`, since there's only one asset and no need to continue-on-error across a list).
62+
4. Wraps `-AssetId` in a one-element `PSCustomObject` ArrayList.
63+
5. `Set-AssetsToDeploymentCluster` is called directly — `Invoke-BatchBasedClusterPinning` is skipped because a single asset never needs batching.
64+
65+
**`ByOuPath`** (bulk via AD OU, single cluster):
66+
1. `Initialize-ApiContext`
67+
2. `Resolve-DeploymentClusterName` resolves `-DeploymentClusterName` to a cluster ID, then `Invoke-ValidateDeploymentClusterId` for that ID.
68+
3. `Get-OUInfoFromApi` — resolves `-OUPath` to an OU entity ID.
69+
4. `Get-AssetsFromOU` — fetches OU members (nested, unless `-DisableNestedOuResolution`), filters to assets only, wrapped in `[System.Collections.ArrayList]@(...)` to guard against PowerShell unwrapping single-item results.
70+
5. `Test-ValidateProvidedAssetsCanBePinned` — validates the whole asset list at once, honoring `-StopOnAssetValidationError`.
71+
6. `Invoke-BatchBasedClusterPinning``Set-AssetsToDeploymentCluster` in batches of 50, all against the single validated cluster.
72+
73+
**`ByTargetSubnet`** (bulk via IPv4 CIDR subnet, single cluster):
74+
1. `Initialize-ApiContext`
75+
2. `Resolve-DeploymentClusterName` resolves `-DeploymentClusterName` to a cluster ID, then `Invoke-ValidateDeploymentClusterId` for that ID.
76+
3. `Get-SubnetHostAddresses` — expands `-TargetSubnet` into every individual host address in the range (including network/broadcast addresses). Warns and requires interactive confirmation above /24 (256 addresses), and hard-stops above /16 (65,536 addresses).
77+
4. `Get-AssetsByHostAddresses` — queries `/assets/monitored` in batches of `$script:SUBNET_BATCH_SIZE` (default 100, not a script parameter) using a `lastIpAddress` filter. Batches run concurrently via `ForEach-Object -Parallel`, up to `-MaxConcurrentBatches` (default 5; set to 1 for sequential behavior) — results are merged into a single list after the parallel block completes (empty batches are expected, not an error). `Invoke-ApiRequest` retries up to 3 times with exponential backoff on HTTP 429 to absorb any rate limiting the added concurrency triggers.
78+
5. `Test-ValidateProvidedAssetsCanBePinned` — validates the whole asset list at once, honoring `-StopOnAssetValidationError`. Assets from `/assets/monitored` already carry `.id`/`.name` in the shape expected, so no normalization step is needed (unlike `ByCsvPath`).
79+
6. `Invoke-BatchBasedClusterPinning``Set-AssetsToDeploymentCluster` in batches of 50, all against the single validated cluster.
80+
81+
**`ByCsvPath`** (bulk via CSV, potentially multiple clusters):
82+
1. `Initialize-ApiContext`
83+
2. `Get-CsvData` — reads and validates the CSV (required columns, non-empty rows). The cluster column is `DeploymentClusterName`, not an ID.
84+
3. Extracts the **unique** `DeploymentClusterName` values present in the CSV, resolves each via `Resolve-DeploymentClusterName`, and runs `Invoke-ValidateDeploymentClusterId` once per resolved cluster ID (not once per row) — building a `$ClusterNameToIdMap` hashtable along the way.
85+
4. CSV rows are normalized into asset-shaped `PSCustomObject`s (`id`, `name`, `DeploymentClusterId`) using `$ClusterNameToIdMap` to look up each row's resolved ID, so downstream validation/pinning functions are mode-agnostic with the OU/AssetId paths (they still key off `DeploymentClusterId`, never the name).
86+
5. `Test-ValidateProvidedAssetsCanBePinned` validates the full normalized asset list in one pass (across all clusters at once), honoring `-StopOnAssetValidationError`.
87+
6. For each unique resolved cluster ID (`$ClusterNameToIdMap.Values`), the validated assets are filtered by `DeploymentClusterId` and passed to `Invoke-BatchBasedClusterPinning``Set-AssetsToDeploymentCluster` (batches of 50) — so each cluster gets its own batched mutation call(s).
88+
89+
## Coding Standards
90+
91+
### Function Documentation
92+
93+
Every function written must include a powershell block comment proceeding it, documenting the function. The comment should include a .SYNOPSIS, .PARAMETER (for every parameter), .OUTPUTS, .NOTES.
94+
95+
For example:
96+
```powershell
97+
<#
98+
.SYNOPSIS
99+
Retrieves detailed information about an asset from the Zero Networks API.
100+
.PARAMETER AssetId
101+
The asset ID to retrieve details for.
102+
.OUTPUTS
103+
Returns the asset entity object from the API response.
104+
.NOTES
105+
Throws an exception if the asset is not found or if the API response is malformed.
106+
#>
107+
```
108+
### Git Commit Message
109+
110+
Every git commit message must follow the widely practiced **Conventional Commits** guidelines. Invoke the `/conventional-commits` skill.

Segment/Segment/Asset Management/Pin Assets To Clusters/DeploymentClusterFieldMappings.json

Lines changed: 0 additions & 95 deletions
This file was deleted.

0 commit comments

Comments
 (0)