Skip to content

Commit 508ee68

Browse files
committed
Add parse_cidr()
1 parent 007090d commit 508ee68

File tree

9 files changed

+1253
-0
lines changed

9 files changed

+1253
-0
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ url = { version = "2.5" }
204204
urlencoding = { version = "2.1" }
205205
# dsc-lib
206206
which = { version = "8.0" }
207+
# dsc-lib
208+
ipnetwork = { version = "0.21" }
207209

208210
# build-only dependencies
209211
# dsc-lib, dsc-lib-registry, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config

dsc/tests/dsc_functions.tests.ps1

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,4 +1293,273 @@ Describe 'tests for function expressions' {
12931293
$expected = "Microsoft.DSC.Debug/Echo:$([Uri]::EscapeDataString($name))"
12941294
$out.results[0].result.actualState.output | Should -BeExactly $expected
12951295
}
1296+
1297+
It 'parseCidr parses IPv4 CIDR notation: <cidr>' -TestCases @(
1298+
@{ cidr = '192.168.1.0/24'; network = '192.168.1.0'; broadcast = '192.168.1.255'; firstUsable = '192.168.1.1'; lastUsable = '192.168.1.254'; netmask = '255.255.255.0'; prefix = 24 }
1299+
@{ cidr = '10.0.0.0/16'; network = '10.0.0.0'; broadcast = '10.0.255.255'; firstUsable = '10.0.0.1'; lastUsable = '10.0.255.254'; netmask = '255.255.0.0'; prefix = 16 }
1300+
@{ cidr = '10.144.0.0/20'; network = '10.144.0.0'; broadcast = '10.144.15.255'; firstUsable = '10.144.0.1'; lastUsable = '10.144.15.254'; netmask = '255.255.240.0'; prefix = 20 }
1301+
@{ cidr = '172.16.0.0/12'; network = '172.16.0.0'; broadcast = '172.31.255.255'; firstUsable = '172.16.0.1'; lastUsable = '172.31.255.254'; netmask = '255.240.0.0'; prefix = 12 }
1302+
@{ cidr = '192.168.1.100/32'; network = '192.168.1.100'; broadcast = '192.168.1.100'; firstUsable = '192.168.1.100'; lastUsable = '192.168.1.100'; netmask = '255.255.255.255'; prefix = 32 }
1303+
) {
1304+
param($cidr, $network, $broadcast, $firstUsable, $lastUsable, $netmask, $prefix)
1305+
1306+
$config_yaml = @"
1307+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1308+
resources:
1309+
- name: Echo
1310+
type: Microsoft.DSC.Debug/Echo
1311+
properties:
1312+
output: "[parseCidr('$cidr')]"
1313+
"@
1314+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1315+
$LASTEXITCODE | Should -Be 0
1316+
$result = $out.results[0].result.actualState.output
1317+
$result.network | Should -BeExactly $network
1318+
$result.netmask | Should -BeExactly $netmask
1319+
$result.broadcast | Should -BeExactly $broadcast
1320+
$result.firstUsable | Should -BeExactly $firstUsable
1321+
$result.lastUsable | Should -BeExactly $lastUsable
1322+
$result.cidr | Should -Be $prefix
1323+
}
1324+
1325+
It 'parseCidr parses IPv6 CIDR notation: <cidr>' -TestCases @(
1326+
@{ cidr = '2001:db8::/32'; network = '2001:db8::'; prefix = 32 }
1327+
@{ cidr = 'fe80::/64'; network = 'fe80::'; prefix = 64 }
1328+
@{ cidr = '2001:db8::1/128'; network = '2001:db8::1'; prefix = 128 }
1329+
) {
1330+
param($cidr, $network, $prefix)
1331+
1332+
$config_yaml = @"
1333+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1334+
resources:
1335+
- name: Echo
1336+
type: Microsoft.DSC.Debug/Echo
1337+
properties:
1338+
output: "[parseCidr('$cidr')]"
1339+
"@
1340+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1341+
$LASTEXITCODE | Should -Be 0
1342+
$result = $out.results[0].result.actualState.output
1343+
$result.network | Should -BeExactly $network
1344+
$result.cidr | Should -Be $prefix
1345+
$result.netmask | Should -Not -BeNullOrEmpty
1346+
$result.broadcast | Should -Not -BeNullOrEmpty
1347+
$result.firstUsable | Should -Not -BeNullOrEmpty
1348+
$result.lastUsable | Should -Not -BeNullOrEmpty
1349+
}
1350+
1351+
It 'parseCidr handles CIDR with host bits set' {
1352+
$config_yaml = @"
1353+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1354+
resources:
1355+
- name: Echo
1356+
type: Microsoft.DSC.Debug/Echo
1357+
properties:
1358+
output: "[parseCidr('192.168.1.100/24')]"
1359+
"@
1360+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1361+
$LASTEXITCODE | Should -Be 0
1362+
$result = $out.results[0].result.actualState.output
1363+
$result.network | Should -BeExactly '192.168.1.0'
1364+
$result.broadcast | Should -BeExactly '192.168.1.255'
1365+
}
1366+
1367+
It 'parseCidr fails with invalid CIDR: <cidr>' -TestCases @(
1368+
@{ cidr = 'invalid'; errorMatch = 'Invalid CIDR notation' }
1369+
@{ cidr = '192.168.1.0/33'; errorMatch = 'Invalid CIDR notation' }
1370+
@{ cidr = '192.168.1.256/24'; errorMatch = 'Invalid CIDR notation' }
1371+
) {
1372+
param($cidr, $errorMatch)
1373+
1374+
$config_yaml = @"
1375+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1376+
resources:
1377+
- name: Echo
1378+
type: Microsoft.DSC.Debug/Echo
1379+
properties:
1380+
output: "[parseCidr('$cidr')]"
1381+
"@
1382+
$errorContent = & { $config_yaml | dsc config get -f - 2>&1 } | Out-String
1383+
$LASTEXITCODE | Should -Be 2
1384+
$errorContent | Should -Match $errorMatch
1385+
}
1386+
1387+
It 'cidrSubnet splits IPv4 network into subnets: <network>/<newCidr> index <index>' -TestCases @(
1388+
@{ network = '10.144.0.0/20'; newCidr = 24; index = 0; expected = '10.144.0.0/24' }
1389+
@{ network = '10.144.0.0/20'; newCidr = 24; index = 1; expected = '10.144.1.0/24' }
1390+
@{ network = '10.144.0.0/20'; newCidr = 24; index = 2; expected = '10.144.2.0/24' }
1391+
@{ network = '10.144.0.0/20'; newCidr = 24; index = 3; expected = '10.144.3.0/24' }
1392+
@{ network = '10.144.0.0/20'; newCidr = 24; index = 4; expected = '10.144.4.0/24' }
1393+
@{ network = '10.144.0.0/20'; newCidr = 24; index = 15; expected = '10.144.15.0/24' }
1394+
@{ network = '10.0.0.0/16'; newCidr = 18; index = 0; expected = '10.0.0.0/18' }
1395+
@{ network = '10.0.0.0/16'; newCidr = 18; index = 1; expected = '10.0.64.0/18' }
1396+
@{ network = '192.168.0.0/24'; newCidr = 28; index = 0; expected = '192.168.0.0/28' }
1397+
) {
1398+
param($network, $newCidr, $index, $expected)
1399+
1400+
$config_yaml = @"
1401+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1402+
resources:
1403+
- name: Echo
1404+
type: Microsoft.DSC.Debug/Echo
1405+
properties:
1406+
output: "[cidrSubnet('$network', $newCidr, $index)]"
1407+
"@
1408+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1409+
$LASTEXITCODE | Should -Be 0
1410+
$out.results[0].result.actualState.output | Should -BeExactly $expected
1411+
}
1412+
1413+
It 'cidrSubnet splits IPv6 network into subnets' {
1414+
$config_yaml = @"
1415+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1416+
resources:
1417+
- name: Echo
1418+
type: Microsoft.DSC.Debug/Echo
1419+
properties:
1420+
output: "[cidrSubnet('2001:db8::/32', 48, 0)]"
1421+
"@
1422+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1423+
$LASTEXITCODE | Should -Be 0
1424+
$out.results[0].result.actualState.output | Should -BeExactly '2001:db8::/48'
1425+
}
1426+
1427+
It 'cidrSubnet with same prefix returns original network' {
1428+
$config_yaml = @"
1429+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1430+
resources:
1431+
- name: Echo
1432+
type: Microsoft.DSC.Debug/Echo
1433+
properties:
1434+
output: "[cidrSubnet('10.144.0.0/20', 20, 0)]"
1435+
"@
1436+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1437+
$LASTEXITCODE | Should -Be 0
1438+
$out.results[0].result.actualState.output | Should -BeExactly '10.144.0.0/20'
1439+
}
1440+
1441+
It 'cidrSubnet fails with invalid parameters: <testName>' -TestCases @(
1442+
@{ testName = 'index out of range'; network = '10.144.0.0/20'; newCidr = 24; index = 16; errorMatch = 'out of range' }
1443+
@{ testName = 'negative index'; network = '10.144.0.0/20'; newCidr = 24; index = -1; errorMatch = 'negative' }
1444+
@{ testName = 'new CIDR too small'; network = '10.144.0.0/20'; newCidr = 16; index = 0; errorMatch = 'equal to or larger' }
1445+
@{ testName = 'invalid IPv4 prefix'; network = '10.144.0.0/20'; newCidr = 33; index = 0; errorMatch = 'Invalid IPv4 prefix' }
1446+
@{ testName = 'invalid IPv6 prefix'; network = '2001:db8::/32'; newCidr = 129; index = 0; errorMatch = 'Invalid IPv6 prefix' }
1447+
@{ testName = 'invalid CIDR format'; network = 'invalid'; newCidr = 24; index = 0; errorMatch = 'Invalid CIDR notation' }
1448+
) {
1449+
param($testName, $network, $newCidr, $index, $errorMatch)
1450+
1451+
$config_yaml = @"
1452+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1453+
resources:
1454+
- name: Echo
1455+
type: Microsoft.DSC.Debug/Echo
1456+
properties:
1457+
output: "[cidrSubnet('$network', $newCidr, $index)]"
1458+
"@
1459+
$errorContent = & { $config_yaml | dsc config get -f - 2>&1 } | Out-String
1460+
$LASTEXITCODE | Should -Be 2
1461+
$errorContent | Should -Match $errorMatch
1462+
}
1463+
1464+
It 'cidrSubnet can be used with parseCidr' {
1465+
$config_yaml = @"
1466+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1467+
resources:
1468+
- name: Echo
1469+
type: Microsoft.DSC.Debug/Echo
1470+
properties:
1471+
output: "[parseCidr(cidrSubnet('10.144.0.0/20', 24, 2))]"
1472+
"@
1473+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1474+
$LASTEXITCODE | Should -Be 0
1475+
$result = $out.results[0].result.actualState.output
1476+
$result.network | Should -BeExactly '10.144.2.0'
1477+
$result.cidr | Should -Be 24
1478+
}
1479+
1480+
It 'cidrHost returns usable host IP: <network> index <index>' -TestCases @(
1481+
@{ network = '192.168.1.0/24'; index = 0; expected = '192.168.1.1' }
1482+
@{ network = '192.168.1.0/24'; index = 1; expected = '192.168.1.2' }
1483+
@{ network = '192.168.1.0/24'; index = 10; expected = '192.168.1.11' }
1484+
@{ network = '192.168.1.0/24'; index = 253; expected = '192.168.1.254' }
1485+
@{ network = '10.0.0.0/16'; index = 0; expected = '10.0.0.1' }
1486+
@{ network = '10.0.0.0/16'; index = 99; expected = '10.0.0.100' }
1487+
@{ network = '10.0.0.0/16'; index = 255; expected = '10.0.1.0' }
1488+
) {
1489+
param($network, $index, $expected)
1490+
1491+
$config_yaml = @"
1492+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1493+
resources:
1494+
- name: Echo
1495+
type: Microsoft.DSC.Debug/Echo
1496+
properties:
1497+
output: "[cidrHost('$network', $index)]"
1498+
"@
1499+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1500+
$LASTEXITCODE | Should -Be 0
1501+
$out.results[0].result.actualState.output | Should -BeExactly $expected
1502+
}
1503+
1504+
It 'cidrHost handles /31 point-to-point' {
1505+
$config_yaml = @"
1506+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1507+
resources:
1508+
- name: Echo
1509+
type: Microsoft.DSC.Debug/Echo
1510+
properties:
1511+
output: "[cidrHost('192.168.1.0/31', 0)]"
1512+
"@
1513+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1514+
$LASTEXITCODE | Should -Be 0
1515+
$out.results[0].result.actualState.output | Should -BeExactly '192.168.1.0'
1516+
1517+
$config_yaml = @"
1518+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1519+
resources:
1520+
- name: Echo
1521+
type: Microsoft.DSC.Debug/Echo
1522+
properties:
1523+
output: "[cidrHost('192.168.1.0/31', 1)]"
1524+
"@
1525+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1526+
$LASTEXITCODE | Should -Be 0
1527+
$out.results[0].result.actualState.output | Should -BeExactly '192.168.1.1'
1528+
}
1529+
1530+
It 'cidrHost works with IPv6' {
1531+
$config_yaml = @"
1532+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1533+
resources:
1534+
- name: Echo
1535+
type: Microsoft.DSC.Debug/Echo
1536+
properties:
1537+
output: "[cidrHost('2001:db8::/64', 0)]"
1538+
"@
1539+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
1540+
$LASTEXITCODE | Should -Be 0
1541+
$out.results[0].result.actualState.output | Should -BeExactly '2001:db8::1'
1542+
}
1543+
1544+
It 'cidrHost fails with invalid parameters: <testName>' -TestCases @(
1545+
@{ testName = '/32 has no usable hosts'; network = '192.168.1.1/32'; index = 0; errorMatch = 'no usable host' }
1546+
@{ testName = '/128 has no usable hosts'; network = '2001:db8::1/128'; index = 0; errorMatch = 'no usable host' }
1547+
@{ testName = 'index out of range'; network = '192.168.1.0/24'; index = 254; errorMatch = 'out of range' }
1548+
@{ testName = 'negative index'; network = '192.168.1.0/24'; index = -1; errorMatch = 'negative' }
1549+
@{ testName = 'invalid CIDR'; network = 'invalid'; index = 0; errorMatch = 'Invalid CIDR notation' }
1550+
) {
1551+
param($testName, $network, $index, $errorMatch)
1552+
1553+
$config_yaml = @"
1554+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1555+
resources:
1556+
- name: Echo
1557+
type: Microsoft.DSC.Debug/Echo
1558+
properties:
1559+
output: "[cidrHost('$network', $index)]"
1560+
"@
1561+
$errorContent = & { $config_yaml | dsc config get -f - 2>&1 } | Out-String
1562+
$LASTEXITCODE | Should -Be 2
1563+
$errorContent | Should -Match $errorMatch
1564+
}
12961565
}

lib/dsc-lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ uuid = { workspace = true }
4343
url = { workspace = true }
4444
urlencoding = { workspace = true }
4545
which = { workspace = true }
46+
ipnetwork = { workspace = true }
4647
# workspace crate dependencies
4748
dsc-lib-osinfo = { workspace = true }
4849
dsc-lib-security_context = { workspace = true }

lib/dsc-lib/locales/en-us.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,37 @@ keyNotObject = "Parameter '%{key}' is not an object"
463463
keyNotArray = "Parameter '%{key}' is not an array"
464464
keyNotFound = "Parameter '%{key}' not found in context"
465465

466+
[functions.parseCidr]
467+
description = "Parses an IP address range in CIDR notation and returns network properties"
468+
invoked = "parseCidr function"
469+
invalidCidr = "Invalid CIDR notation: '%{cidr}'. Expected format: IP/prefix (e.g., '192.168.1.0/24' or '2001:db8::/32')"
470+
471+
[functions.cidrHost]
472+
description = "Calculates the usable IP address of the host with the specified index on the specified IP address range in CIDR notation"
473+
invoked = "cidrHost function"
474+
invalidNetwork = "Network parameter must be a string"
475+
invalidHostIndex = "HostIndex parameter must be an integer"
476+
negativeHostIndex = "HostIndex cannot be negative"
477+
invalidCidr = "Invalid CIDR notation: '%{cidr}'. Expected format: IP/prefix (e.g., '192.168.1.0/24' or '2001:db8::/32')"
478+
noUsableHosts = "Network has no usable host addresses (single IP address)"
479+
hostIndexOutOfRange = "Host index %{index} is out of range. Maximum index is %{maxIndex}"
480+
hostCalculationFailed = "Failed to calculate host address"
481+
482+
[functions.cidrSubnet]
483+
description = "Splits the specified IP address range in CIDR notation into subnets with a new CIDR value and returns the IP address range of the subnet with the specified index"
484+
invoked = "cidrSubnet function"
485+
invalidNetwork = "Network parameter must be a string"
486+
invalidNewCidr = "NewCIDR parameter must be an integer"
487+
invalidSubnetIndex = "SubnetIndex parameter must be an integer"
488+
negativeSubnetIndex = "SubnetIndex cannot be negative"
489+
invalidCidr = "Invalid CIDR notation: '%{cidr}'. Expected format: IP/prefix (e.g., '192.168.1.0/24' or '2001:db8::/32')"
490+
invalidPrefixV4 = "Invalid IPv4 prefix: %{prefix}. Must be between 0 and 32"
491+
invalidPrefixV6 = "Invalid IPv6 prefix: %{prefix}. Must be between 0 and 128"
492+
newCidrTooSmall = "New CIDR prefix (%{newCidr}) must be equal to or larger than the current CIDR prefix (%{currentCidr})"
493+
subnetIndexOutOfRange = "Subnet index %{index} is out of range. Maximum index is %{maxIndex}"
494+
tooManySubnets = "The difference between new and current CIDR is too large (>32 bits). This would create too many subnets"
495+
subnetCreationFailed = "Failed to create subnet"
496+
466497
[functions.path]
467498
description = "Concatenates multiple strings into a file path"
468499
traceArgs = "Executing path function with args: %{args}"

0 commit comments

Comments
 (0)