18 Commits

Author SHA1 Message Date
283fd7a703 Merge pull request 'Enhance BaseCmdlet class with additional async methods and improve documentation for cmdlet functionality' (#8) from develop into main
Some checks failed
CI/CD Pipeline / Build and Test (push) Successful in 1m8s
CI/CD Pipeline / Package Module (push) Successful in 44s
CI/CD Pipeline / Create Release (push) Failing after 15s
Reviewed-on: #8
2026-01-19 18:01:28 +03:00
3f6a59abad Enhance BaseCmdlet class with additional async methods and improve documentation for cmdlet functionality
All checks were successful
CI/CD Pipeline / Build and Test (push) Successful in 1m12s
CI/CD Pipeline / Package Module (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
CI/CD Pipeline / Build and Test (pull_request) Successful in 1m6s
CI/CD Pipeline / Package Module (pull_request) Has been skipped
CI/CD Pipeline / Create Release (pull_request) Has been skipped
2026-01-19 17:58:44 +03:00
7b9d229289 Merge pull request 'develop' (#7) from develop into main
All checks were successful
CI/CD Pipeline / Build and Test (push) Successful in 1m5s
CI/CD Pipeline / Package Module (push) Successful in 43s
CI/CD Pipeline / Create Release (push) Has been skipped
Reviewed-on: #7
2026-01-19 17:35:19 +03:00
bc86ad2965 Add IsExternalInit type, BaseCmdlet class, and SessionStatus enum
All checks were successful
CI/CD Pipeline / Build and Test (push) Successful in 1m3s
CI/CD Pipeline / Package Module (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
CI/CD Pipeline / Build and Test (pull_request) Successful in 1m7s
CI/CD Pipeline / Package Module (pull_request) Has been skipped
CI/CD Pipeline / Create Release (pull_request) Has been skipped
Introduce the IsExternalInit type for C# 9 records compatibility in .NET Standard 2.1. Implement BaseCmdlet class to provide common functionality for cmdlets, including methods for writing results and handling async operations. Add SessionStatus enum to represent API session states.
2026-01-19 17:25:41 +03:00
f56784f2aa Refactor IPAM model classes to use records for Address, Subnetwork, Vlan, Vrf, Section, Tag, Domain, Nameserver, and Session; enhance documentation and implement value equality for records. 2026-01-19 17:25:18 +03:00
694822f0d6 Update Gitea CI workflow to downgrade upload and download artifact actions from v4 to v3
All checks were successful
CI/CD Pipeline / Build and Test (push) Successful in 1m18s
CI/CD Pipeline / Package Module (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
2026-01-19 17:04:22 +03:00
df30851a3a Update Gitea CI workflow to restore class library and test project dependencies separately
Some checks failed
CI/CD Pipeline / Build and Test (push) Failing after 1m6s
CI/CD Pipeline / Package Module (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
2026-01-19 17:01:30 +03:00
88e4e12e3d Merge pull request '2026-01-19-nah9' (#6) from 2026-01-19-nah9 into main
Some checks failed
CI/CD Pipeline / Build and Test (push) Failing after 34s
CI/CD Pipeline / Package Module (push) Has been skipped
CI/CD Pipeline / Create Release (push) Has been skipped
Reviewed-on: #6
2026-01-19 16:57:01 +03:00
6541cf18c0 Update Gitea CI workflow to use updated token secret for release creation
Some checks failed
CI/CD Pipeline / Build and Test (pull_request) Failing after 1m0s
CI/CD Pipeline / Package Module (pull_request) Has been skipped
CI/CD Pipeline / Create Release (pull_request) Has been skipped
2026-01-19 16:38:05 +03:00
0b237c6d1c Enhance unit tests for Address, Domain, Nameserver, Section, Session, Subnetwork, Tag, Vlan, and Vrf models; implement mock classes for HTTP requests and cmdlet testing. 2026-01-19 14:54:55 +03:00
40e3c31c6f Remove Jenkinsfile and add unit tests for various models including Address, Domain, Nameserver, Section, Session, Subnetwork, Tag, Vlan, and Vrf. Introduce mock classes for HTTP requests and cmdlet testing. 2026-01-19 14:46:09 +03:00
114267b1d5 Added assembly name and root namespace to project file; updated session and vlan classes with new properties; modified module manifest to reference DLL and updated exported cmdlets; corrected property name in address class. 2026-01-19 14:45:31 +03:00
010cce0fd8 Refactored subnet object methods and enhanced related documentation 2026-01-19 14:44:52 +03:00
f8f240e313 Updated subnet object handling and improved documentation 2026-01-19 14:44:10 +03:00
7f856b1d09 New address in subnet by subnet object 2022-12-30 14:43:46 +03:00
31b56c75e1 jenkinsfile content replace 2022-12-27 09:39:15 +03:00
7f0d14fc25 jenkinsfile 2022-12-27 09:31:54 +03:00
00975bb1c5 jenkinsfile 2022-12-27 09:31:33 +03:00
59 changed files with 5579 additions and 436 deletions

124
.gitea/workflows/ci.yaml Normal file
View File

@@ -0,0 +1,124 @@
name: CI/CD Pipeline
on:
push:
branches:
- main
- master
- develop
tags:
- 'v*'
pull_request:
branches:
- main
- master
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore class library
run: dotnet restore classlib/classlib.csproj
- name: Restore test project
run: dotnet restore classlib.tests/classlib.tests.csproj
- name: Build class library
run: dotnet build classlib/classlib.csproj --configuration Release --no-restore
- name: Build test project
run: dotnet build classlib.tests/classlib.tests.csproj --configuration Release --no-restore
- name: Run tests
run: dotnet test classlib.tests/classlib.tests.csproj --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage"
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: classlib.tests/TestResults/
retention-days: 30
package:
name: Package Module
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build Release
run: dotnet build classlib/classlib.csproj --configuration Release
- name: Create module package
run: |
mkdir -p output/ps.ipam
# Copy compiled DLL
cp classlib/bin/Release/netstandard2.1/ps.ipam.dll output/ps.ipam/
# Copy module manifest and related files
cp ps.ipam.psd1 output/ps.ipam/
cp ps.ipam.psm1 output/ps.ipam/
cp LICENSE output/ps.ipam/
cp README.md output/ps.ipam/
# Copy types directory
cp -r types output/ps.ipam/
# Copy functions directory
cp -r functions output/ps.ipam/
# Copy images directory
cp -r images output/ps.ipam/
- name: Upload module artifact
uses: actions/upload-artifact@v3
with:
name: ps.ipam-module
path: output/ps.ipam/
retention-days: 90
release:
name: Create Release
runs-on: ubuntu-latest
needs: package
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download module artifact
uses: actions/download-artifact@v3
with:
name: ps.ipam-module
path: ps.ipam
- name: Create release archive
run: |
zip -r ps.ipam-${{ github.ref_name }}.zip ps.ipam/
tar -czvf ps.ipam-${{ github.ref_name }}.tar.gz ps.ipam/
- name: Create Gitea Release
uses: actions/gitea-release-action@v1
with:
token: ${{ secrets.TOKEN }}
files: |
ps.ipam-${{ github.ref_name }}.zip
ps.ipam-${{ github.ref_name }}.tar.gz
title: Release ${{ github.ref_name }}
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}

View File

@@ -0,0 +1,330 @@
namespace PS.IPAM.Tests.Cmdlets;
using System.Net;
using FluentAssertions;
using PS.IPAM;
using PS.IPAM.Cmdlets;
using PS.IPAM.Helpers;
using PS.IPAM.Tests.Mocks;
using Xunit;
/// <summary>
/// Tests for the GetAddressCmdlet.
/// Note: Full cmdlet testing with parameter sets requires a PowerShell runspace.
/// These tests focus on verifying the cmdlet structure and the underlying RequestHelper functionality.
/// </summary>
[Collection("Sequential")]
public class GetAddressCmdletTests : IDisposable
{
private MockHttpMessageHandler? _mockHandler;
public GetAddressCmdletTests()
{
// Clean state before each test
SessionManager.CloseSession();
RequestHelper.TestHttpHandler = null;
}
public void Dispose()
{
// Clean up after each test
SessionManager.CloseSession();
RequestHelper.TestHttpHandler = null;
_mockHandler?.ForceDispose();
}
private void SetupSession()
{
SessionManager.CreateSessionWithToken("https://ipam.example.com", "testapp", "test-token");
}
private MockHttpMessageHandler SetupMockHandler()
{
_mockHandler = new MockHttpMessageHandler();
RequestHelper.TestHttpHandler = _mockHandler;
return _mockHandler;
}
[Fact]
public void GetAddressCmdlet_Exists()
{
// Verify the cmdlet class exists and can be instantiated
var cmdlet = new GetAddressCmdlet();
cmdlet.Should().NotBeNull();
}
[Fact]
public void GetAddressCmdlet_IdProperty_Exists()
{
var cmdlet = new GetAddressCmdlet();
cmdlet.Id = 42;
cmdlet.Id.Should().Be(42);
}
[Fact]
public void GetAddressCmdlet_IPProperty_Exists()
{
var cmdlet = new GetAddressCmdlet();
cmdlet.IP = IPAddress.Parse("192.168.1.100");
cmdlet.IP.Should().Be(IPAddress.Parse("192.168.1.100"));
}
[Fact]
public void GetAddressCmdlet_HostNameProperty_Exists()
{
var cmdlet = new GetAddressCmdlet();
cmdlet.HostName = "server01.example.com";
cmdlet.HostName.Should().Be("server01.example.com");
}
[Fact]
public void GetAddressCmdlet_HostBaseProperty_Exists()
{
var cmdlet = new GetAddressCmdlet();
cmdlet.HostBase = "server";
cmdlet.HostBase.Should().Be("server");
}
[Fact]
public void GetAddressCmdlet_TagIdProperty_Exists()
{
var cmdlet = new GetAddressCmdlet();
cmdlet.TagId = 2;
cmdlet.TagId.Should().Be(2);
}
[Fact]
public void GetAddressCmdlet_SubnetIdProperty_Exists()
{
var cmdlet = new GetAddressCmdlet();
cmdlet.SubnetId = 10;
cmdlet.SubnetId.Should().Be(10);
}
[Fact]
public void GetAddressCmdlet_SubnetCIDRProperty_Exists()
{
var cmdlet = new GetAddressCmdlet();
cmdlet.SubnetCIDR = "192.168.1.0/24";
cmdlet.SubnetCIDR.Should().Be("192.168.1.0/24");
}
[Fact]
public void GetAddressCmdlet_InheritsFromBaseCmdlet()
{
var cmdlet = new GetAddressCmdlet();
cmdlet.Should().BeAssignableTo<BaseCmdlet>();
}
// Test the underlying RequestHelper functionality that the cmdlet uses
[Fact]
public async Task RequestHelper_GetAddressById_ReturnsAddress()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
var addressJson = @"{
""id"": 1,
""subnetId"": 10,
""ip"": ""192.168.1.100"",
""hostname"": ""server01"",
""description"": ""Test server""
}";
handler.WithSuccessResponse(addressJson);
// Act
var result = await RequestHelper.InvokeRequest(
"GET",
ApiController.Addresses,
ModelType.Address,
null,
null,
new[] { "1" }
);
// Assert
result.Should().BeOfType<Address>();
var address = (Address)result!;
address.Id.Should().Be(1);
address.Ip.Should().Be("192.168.1.100");
address.Hostname.Should().Be("server01");
}
[Fact]
public async Task RequestHelper_GetAddressById_BuildsCorrectUri()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse(@"{""id"": 42, ""ip"": ""10.0.0.1""}");
// Act
await RequestHelper.InvokeRequest(
"GET",
ApiController.Addresses,
ModelType.Address,
null,
null,
new[] { "42" }
);
// Assert
handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/42/");
}
[Fact]
public async Task RequestHelper_SearchByIP_BuildsCorrectUri()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse(@"{""id"": 1, ""ip"": ""192.168.1.50""}");
// Act
await RequestHelper.InvokeRequest(
"GET",
ApiController.Addresses,
ModelType.Address,
null,
null,
new[] { "search", "192.168.1.50" }
);
// Assert
handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/search/192.168.1.50/");
}
[Fact]
public async Task RequestHelper_SearchByHostname_BuildsCorrectUri()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse(@"{""id"": 1, ""ip"": ""10.0.0.5"", ""hostname"": ""myserver.example.com""}");
// Act
await RequestHelper.InvokeRequest(
"GET",
ApiController.Addresses,
ModelType.Address,
null,
null,
new[] { "search_hostname", "myserver.example.com" }
);
// Assert
handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/search_hostname/myserver.example.com/");
}
[Fact]
public async Task RequestHelper_GetSubnetAddresses_BuildsCorrectUri()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse(@"[{""id"": 1, ""ip"": ""192.168.1.1""}]");
// Act
await RequestHelper.InvokeRequest(
"GET",
ApiController.Subnets,
ModelType.Address,
null,
null,
new[] { "10", "addresses" }
);
// Assert
handler.LastRequest!.RequestUri!.ToString().Should().Contain("/subnets/10/addresses/");
}
[Fact]
public async Task RequestHelper_GetAddressesByTag_BuildsCorrectUri()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse(@"[{""id"": 1, ""ip"": ""10.0.0.1"", ""tag"": 2}]");
// Act
await RequestHelper.InvokeRequest(
"GET",
ApiController.Addresses,
ModelType.Address,
null,
null,
new[] { "tags", "2", "addresses" }
);
// Assert
handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/tags/2/addresses/");
}
[Fact]
public async Task RequestHelper_ReturnsMultipleAddresses()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse(@"[
{""id"": 1, ""ip"": ""192.168.1.1""},
{""id"": 2, ""ip"": ""192.168.1.2""},
{""id"": 3, ""ip"": ""192.168.1.3""}
]");
// Act
var result = await RequestHelper.InvokeRequest(
"GET",
ApiController.Subnets,
ModelType.Address,
null,
null,
new[] { "10", "addresses" }
);
// Assert
result.Should().BeAssignableTo<System.Collections.IEnumerable>();
var addresses = ((System.Collections.IEnumerable)result!).Cast<object>().ToList();
addresses.Should().HaveCount(3);
addresses.Should().AllBeOfType<Address>();
}
[Fact]
public async Task RequestHelper_With404Response_ReturnsNull()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithNotFoundResponse();
// Act
var result = await RequestHelper.InvokeRequest(
"GET",
ApiController.Addresses,
ModelType.Address,
null,
null,
new[] { "999" }
);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task RequestHelper_WithNoSession_ThrowsException()
{
// Arrange - no session set up
// Act
var action = async () => await RequestHelper.InvokeRequest(
"GET",
ApiController.Addresses,
ModelType.Address
);
// Assert
await action.Should().ThrowAsync<InvalidOperationException>().WithMessage("No session available!");
}
}

View File

@@ -0,0 +1,110 @@
namespace PS.IPAM.Tests.Cmdlets;
using FluentAssertions;
using PS.IPAM;
using PS.IPAM.Cmdlets;
using PS.IPAM.Helpers;
using Xunit;
/// <summary>
/// Tests for the NewSessionCmdlet.
/// Note: Full cmdlet testing with parameter sets requires a PowerShell runspace.
/// These tests focus on verifying the cmdlet structure and basic functionality.
/// </summary>
[Collection("Sequential")]
public class NewSessionCmdletTests : IDisposable
{
public NewSessionCmdletTests()
{
// Clean state before each test
SessionManager.CloseSession();
}
public void Dispose()
{
// Clean up after each test
SessionManager.CloseSession();
}
[Fact]
public void NewSessionCmdlet_Exists()
{
// Verify the cmdlet class exists and can be instantiated
var cmdlet = new NewSessionCmdlet();
cmdlet.Should().NotBeNull();
}
[Fact]
public void NewSessionCmdlet_URLProperty_Exists()
{
var cmdlet = new NewSessionCmdlet();
cmdlet.URL = "https://ipam.example.com";
cmdlet.URL.Should().Be("https://ipam.example.com");
}
[Fact]
public void NewSessionCmdlet_AppIDProperty_Exists()
{
var cmdlet = new NewSessionCmdlet();
cmdlet.AppID = "testapp";
cmdlet.AppID.Should().Be("testapp");
}
[Fact]
public void NewSessionCmdlet_TokenProperty_Exists()
{
var cmdlet = new NewSessionCmdlet();
cmdlet.Token = "my-api-token";
cmdlet.Token.Should().Be("my-api-token");
}
[Fact]
public void NewSessionCmdlet_CredentialsProperty_Exists()
{
var cmdlet = new NewSessionCmdlet();
cmdlet.Credentials = null;
cmdlet.Credentials.Should().BeNull();
}
[Fact]
public void NewSessionCmdlet_IgnoreSSLProperty_Exists()
{
var cmdlet = new NewSessionCmdlet();
// SwitchParameter defaults to false
cmdlet.IgnoreSSL.IsPresent.Should().BeFalse();
// Setting it to true
var switchParam = new System.Management.Automation.SwitchParameter(true);
cmdlet.IgnoreSSL = switchParam;
// Note: SwitchParameter is a struct, so getting the value back may not work as expected
// Just verify the property exists and can be set
}
[Fact]
public void SessionManager_CreateSessionWithToken_WorksCorrectly()
{
// This tests the underlying functionality that the cmdlet uses
var session = SessionManager.CreateSessionWithToken(
"https://ipam.example.com",
"testapp",
"my-api-token"
);
session.Should().NotBeNull();
session.URL.Should().Be("https://ipam.example.com");
session.AppID.Should().Be("testapp");
session.Token.Should().Be("my-api-token");
session.AuthType.Should().Be(AuthType.Token);
session.Expires.Should().BeNull();
session.Credentials.Should().BeNull();
// Verify it was set as current session
SessionManager.CurrentSession.Should().BeSameAs(session);
}
[Fact]
public void NewSessionCmdlet_InheritsFromBaseCmdlet()
{
var cmdlet = new NewSessionCmdlet();
cmdlet.Should().BeAssignableTo<BaseCmdlet>();
}
}

View File

@@ -0,0 +1,503 @@
namespace PS.IPAM.Tests.Helpers;
using System.Net;
using FluentAssertions;
using PS.IPAM;
using PS.IPAM.Helpers;
using PS.IPAM.Tests.Mocks;
using Xunit;
[Collection("Sequential")]
public class RequestHelperTests : IDisposable
{
private MockHttpMessageHandler? _mockHandler;
public RequestHelperTests()
{
// Clean state before each test
SessionManager.CloseSession();
RequestHelper.TestHttpHandler = null;
}
public void Dispose()
{
// Clean up after each test
SessionManager.CloseSession();
RequestHelper.TestHttpHandler = null;
_mockHandler?.ForceDispose();
}
private void SetupSession(AuthType authType = AuthType.Token)
{
if (authType == AuthType.Token)
{
SessionManager.CreateSessionWithToken("https://ipam.example.com", "testapp", "test-token");
}
else
{
// For credentials auth, we need to set up a session manually with future expiry
var session = new Session(
AuthType.Credentials,
"cred-token",
"testapp",
"https://ipam.example.com",
DateTime.Now.AddHours(1),
null
);
SessionManager.CurrentSession = session;
}
}
private MockHttpMessageHandler SetupMockHandler()
{
_mockHandler = new MockHttpMessageHandler();
RequestHelper.TestHttpHandler = _mockHandler;
return _mockHandler;
}
[Fact]
public async Task InvokeRequest_WithNoSession_ThrowsException()
{
// Arrange - no session set up
// Act
var action = async () => await RequestHelper.InvokeRequest("GET", ApiController.Addresses);
// Assert
await action.Should().ThrowAsync<InvalidOperationException>().WithMessage("No session available!");
}
[Fact]
public async Task InvokeRequest_WithValidSession_BuildsCorrectUri()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse("{\"id\":1,\"ip\":\"192.168.1.1\"}");
// Act
await RequestHelper.InvokeRequest("GET", ApiController.Addresses, ModelType.Address, null, null, new[] { "1" });
// Assert
handler.LastRequest.Should().NotBeNull();
handler.LastRequest!.RequestUri!.ToString().Should().Be("https://ipam.example.com/api/testapp/addresses/1/");
}
[Fact]
public async Task InvokeRequest_WithSubController_BuildsCorrectUri()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse("[]");
// Act
await RequestHelper.InvokeRequest("GET", ApiController.Subnets, null, ApiSubController.Tags, null, new[] { "1" });
// Assert
handler.LastRequest!.RequestUri!.ToString().Should().Contain("/subnets/tags/1/");
}
[Fact]
public async Task InvokeRequest_WithTokenAuth_AddsPhpipamTokenHeader()
{
// Arrange
SetupSession(AuthType.Token);
var handler = SetupMockHandler();
handler.WithSuccessResponse("{}");
// Act
await RequestHelper.InvokeRequest("GET", ApiController.Addresses);
// Assert
handler.GetLastRequestHeader("phpipam-token").Should().Be("test-token");
}
[Fact]
public async Task InvokeRequest_WithCredentialsAuth_AddsTokenHeader()
{
// Arrange
SetupSession(AuthType.Credentials);
var handler = SetupMockHandler();
handler.WithSuccessResponse("{}");
// Act
await RequestHelper.InvokeRequest("GET", ApiController.Addresses);
// Assert
handler.GetLastRequestHeader("token").Should().Be("cred-token");
}
[Fact]
public async Task InvokeRequest_GetMethod_UsesHttpGet()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse("{}");
// Act
await RequestHelper.InvokeRequest("GET", ApiController.Addresses);
// Assert
handler.LastRequest!.Method.Should().Be(HttpMethod.Get);
}
[Fact]
public async Task InvokeRequest_PostMethod_UsesHttpPost()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse("{\"id\":1}");
// Act
await RequestHelper.InvokeRequest("POST", ApiController.Addresses, null, null, new { ip = "10.0.0.1" });
// Assert
handler.LastRequest!.Method.Should().Be(HttpMethod.Post);
}
[Fact]
public async Task InvokeRequest_PatchMethod_UsesHttpPatch()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse("{\"id\":1}");
// Act
await RequestHelper.InvokeRequest("PATCH", ApiController.Addresses, null, null, new { description = "updated" }, new[] { "1" });
// Assert
handler.LastRequest!.Method.Should().Be(new HttpMethod("PATCH"));
}
[Fact]
public async Task InvokeRequest_DeleteMethod_UsesHttpDelete()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse("{}");
// Act
await RequestHelper.InvokeRequest("DELETE", ApiController.Addresses, null, null, null, new[] { "1" });
// Assert
handler.LastRequest!.Method.Should().Be(HttpMethod.Delete);
}
[Fact]
public async Task InvokeRequest_With404Response_ReturnsNull()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithNotFoundResponse();
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Addresses, ModelType.Address, null, null, new[] { "999" });
// Assert
result.Should().BeNull();
}
[Fact]
public async Task InvokeRequest_WithErrorResponse_ThrowsHttpRequestException()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithErrorResponse(HttpStatusCode.InternalServerError, "Server error");
// Act
var action = async () => await RequestHelper.InvokeRequest("GET", ApiController.Addresses);
// Assert
await action.Should().ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task InvokeRequest_WithAddressType_ReturnsAddressObject()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
var addressJson = @"{
""id"": 1,
""subnetId"": 10,
""ip"": ""192.168.1.100"",
""is_gateway"": false,
""description"": ""Test server"",
""hostname"": ""server01"",
""mac"": ""00:11:22:33:44:55"",
""owner"": ""admin"",
""tag"": 2,
""deviceId"": 0,
""location"": """",
""port"": """",
""note"": """",
""lastSeen"": null,
""excludePing"": false,
""PTRignore"": false,
""PTR"": 0,
""firewallAddressObject"": """",
""editDate"": null,
""customer_id"": 0
}";
handler.WithSuccessResponse(addressJson);
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Addresses, ModelType.Address, null, null, new[] { "1" });
// Assert
result.Should().BeOfType<Address>();
var address = (Address)result!;
address.Id.Should().Be(1);
address.Ip.Should().Be("192.168.1.100");
address.Hostname.Should().Be("server01");
}
[Fact]
public async Task InvokeRequest_WithAddressArray_ReturnsListOfAddresses()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
var addressArrayJson = @"[
{""id"": 1, ""subnetId"": 10, ""ip"": ""192.168.1.1"", ""hostname"": ""host1""},
{""id"": 2, ""subnetId"": 10, ""ip"": ""192.168.1.2"", ""hostname"": ""host2""}
]";
handler.WithSuccessResponse(addressArrayJson);
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Subnets, ModelType.Address, null, null, new[] { "10", "addresses" });
// Assert
result.Should().BeAssignableTo<IEnumerable<object>>();
var addresses = ((IEnumerable<object>)result!).ToList();
addresses.Should().HaveCount(2);
addresses[0].Should().BeOfType<Address>();
((Address)addresses[0]).Ip.Should().Be("192.168.1.1");
}
[Fact]
public async Task InvokeRequest_WithVlanType_ReturnsVlanObject()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
var vlanJson = @"{
""vlanId"": 100,
""domainId"": 1,
""name"": ""Production"",
""number"": 100,
""description"": ""Production VLAN"",
""editDate"": null,
""customer_id"": 0
}";
handler.WithSuccessResponse(vlanJson);
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Vlan, ModelType.Vlan, null, null, new[] { "100" });
// Assert
result.Should().BeOfType<Vlan>();
var vlan = (Vlan)result!;
vlan.Name.Should().Be("Production");
vlan.Number.Should().Be(100);
}
[Fact]
public async Task InvokeRequest_WithSubnetworkType_ReturnsSubnetworkObject()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
var subnetJson = @"{
""id"": 1,
""subnet"": ""192.168.1.0"",
""mask"": 24,
""sectionId"": 1,
""description"": ""Test subnet"",
""linked_subnet"": """",
""firewallAddressObject"": """",
""vrfId"": 0,
""masterSubnetId"": 0,
""allowRequests"": false,
""vlanId"": 100,
""showName"": false,
""deviceId"": 0,
""permissions"": """",
""pingSubnet"": false,
""discoverSubnet"": false,
""resolveDNS"": false,
""DNSrecursive"": false,
""DNSrecords"": false,
""nameserverId"": 0,
""scanAgent"": false,
""isFolder"": false,
""isFull"": false,
""isPool"": false,
""state"": 1,
""threshold"": 0,
""location"": 0,
""editDate"": null,
""lastScan"": null,
""lastDiscovery"": null,
""calculation"": {}
}";
handler.WithSuccessResponse(subnetJson);
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Subnets, ModelType.Subnetwork, null, null, new[] { "1" });
// Assert
result.Should().BeOfType<Subnetwork>();
var subnet = (Subnetwork)result!;
subnet.Subnet.Should().Be("192.168.1.0");
subnet.Mask.Should().Be(24);
subnet.CIDR.Should().Be("192.168.1.0/24");
}
[Fact]
public async Task InvokeRequest_WithSectionType_ReturnsSectionObject()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
var sectionJson = @"{
""id"": 1,
""name"": ""Production"",
""description"": ""Production section"",
""masterSection"": 0,
""permissions"": """",
""strictMode"": false,
""subnetOrdering"": ""default"",
""order"": 1,
""editDate"": null,
""showSubnet"": true,
""showVlan"": true,
""showVRF"": false,
""showSupernetOnly"": false,
""DNS"": 0
}";
handler.WithSuccessResponse(sectionJson);
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Sections, ModelType.Section, null, null, new[] { "1" });
// Assert
result.Should().BeOfType<Section>();
var section = (Section)result!;
section.Name.Should().Be("Production");
}
[Fact]
public async Task InvokeRequest_WithCustomFields_ParsesExtendedData()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
var addressJson = @"{
""id"": 1,
""subnetId"": 10,
""ip"": ""192.168.1.100"",
""hostname"": ""server01"",
""custom_environment"": ""production"",
""custom_owner"": ""team-a""
}";
handler.WithSuccessResponse(addressJson);
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Addresses, ModelType.Address, null, null, new[] { "1" });
// Assert
var address = (Address)result!;
address.ExtendedData.Should().NotBeNull();
var extendedData = address.ExtendedData!;
extendedData.Should().ContainKey("custom_environment");
extendedData.Should().ContainKey("custom_owner");
}
[Fact]
public async Task InvokeRequest_WithNoType_ReturnsDynamicData()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse(@"{""some"": ""data""}");
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Tools);
// Assert
result.Should().NotBeNull();
}
[Fact]
public async Task InvokeRequest_PostWithParameters_SerializesJsonBody()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse(@"{""id"": 1}");
var parameters = new { ip = "10.0.0.1", subnetId = 5, description = "New address" };
// Act
await RequestHelper.InvokeRequest("POST", ApiController.Addresses, null, null, parameters);
// Assert
handler.LastRequest!.Content.Should().NotBeNull();
var content = await handler.LastRequest.Content!.ReadAsStringAsync();
content.Should().Contain("10.0.0.1");
content.Should().Contain("subnetId");
}
[Fact]
public async Task InvokeRequest_SetsAcceptJsonHeader()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithSuccessResponse("{}");
// Act
await RequestHelper.InvokeRequest("GET", ApiController.Addresses);
// Assert
handler.LastRequest!.Headers.Accept.Should().Contain(h => h.MediaType == "application/json");
}
[Fact]
public async Task InvokeRequest_WithEmptyResponse_ReturnsNull()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithResponse(HttpStatusCode.OK, "", "application/json");
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Addresses);
// Assert
result.Should().BeNull();
}
[Fact]
public async Task InvokeRequest_WithNullDataInResponse_ReturnsNull()
{
// Arrange
SetupSession();
var handler = SetupMockHandler();
handler.WithJsonResponse(@"{""code"": 200, ""success"": true, ""data"": null}");
// Act
var result = await RequestHelper.InvokeRequest("GET", ApiController.Addresses, ModelType.Address);
// Assert
result.Should().BeNull();
}
}

View File

@@ -0,0 +1,193 @@
namespace PS.IPAM.Tests.Helpers;
using FluentAssertions;
using PS.IPAM;
using PS.IPAM.Helpers;
using Xunit;
[Collection("Sequential")]
public class SessionManagerTests : IDisposable
{
public SessionManagerTests()
{
// Ensure clean state before each test
SessionManager.CloseSession();
}
public void Dispose()
{
// Clean up after each test
SessionManager.CloseSession();
}
[Fact]
public void GetSessionStatus_WhenNoSession_ReturnsNoSession()
{
// Arrange
SessionManager.CurrentSession = null;
// Act
var result = SessionManager.GetSessionStatus();
// Assert
result.Should().Be(SessionStatus.NoSession);
}
[Fact]
public void GetSessionStatus_WhenSessionWithNullExpires_ReturnsValid()
{
// Arrange
var session = new Session(AuthType.Token, "test-token", "app", "https://test.com", null, null);
SessionManager.CurrentSession = session;
// Act
var result = SessionManager.GetSessionStatus();
// Assert
result.Should().Be(SessionStatus.Valid);
}
[Fact]
public void GetSessionStatus_WhenSessionNotExpired_ReturnsValid()
{
// Arrange
var futureExpiry = DateTime.Now.AddHours(1);
var session = new Session(AuthType.Credentials, "test-token", "app", "https://test.com", futureExpiry, null);
SessionManager.CurrentSession = session;
// Act
var result = SessionManager.GetSessionStatus();
// Assert
result.Should().Be(SessionStatus.Valid);
}
[Fact]
public void GetSessionStatus_WhenSessionExpired_ReturnsExpired()
{
// Arrange
var pastExpiry = DateTime.Now.AddHours(-1);
var session = new Session(AuthType.Credentials, "test-token", "app", "https://test.com", pastExpiry, null);
SessionManager.CurrentSession = session;
// Act
var result = SessionManager.GetSessionStatus();
// Assert
result.Should().Be(SessionStatus.Expired);
}
[Fact]
public void CreateSessionWithToken_CreatesValidSession()
{
// Arrange
var url = "https://ipam.example.com";
var appId = "myApp";
var token = "static-api-token";
// Act
var session = SessionManager.CreateSessionWithToken(url, appId, token);
// Assert
session.Should().NotBeNull();
session.URL.Should().Be(url);
session.AppID.Should().Be(appId);
session.Token.Should().Be(token);
session.AuthType.Should().Be(AuthType.Token);
session.Expires.Should().BeNull();
session.Credentials.Should().BeNull();
}
[Fact]
public void CreateSessionWithToken_SetsCurrentSession()
{
// Arrange
var url = "https://ipam.example.com";
var appId = "myApp";
var token = "api-token";
// Act
var session = SessionManager.CreateSessionWithToken(url, appId, token);
// Assert
SessionManager.CurrentSession.Should().BeSameAs(session);
}
[Fact]
public void CloseSession_ClearsCurrentSession()
{
// Arrange
SessionManager.CreateSessionWithToken("https://test.com", "app", "token");
SessionManager.CurrentSession.Should().NotBeNull();
// Act
SessionManager.CloseSession();
// Assert
SessionManager.CurrentSession.Should().BeNull();
}
[Fact]
public void CloseSession_WhenNoSession_DoesNotThrow()
{
// Arrange
SessionManager.CurrentSession = null;
// Act
var action = () => SessionManager.CloseSession();
// Assert
action.Should().NotThrow();
}
[Fact]
public void CurrentSession_CanBeSetDirectly()
{
// Arrange
var session = new Session(AuthType.Token, "token", "app", "https://test.com", null, null);
// Act
SessionManager.CurrentSession = session;
// Assert
SessionManager.CurrentSession.Should().BeSameAs(session);
}
[Fact]
public void CreateHttpClient_WithoutIgnoreSsl_ReturnsHttpClient()
{
// Act
using var client = SessionManager.CreateHttpClient(false);
// Assert
client.Should().NotBeNull();
client.Should().BeOfType<HttpClient>();
}
[Fact]
public void CreateHttpClient_WithIgnoreSsl_ReturnsHttpClient()
{
// Act
using var client = SessionManager.CreateHttpClient(true);
// Assert
client.Should().NotBeNull();
client.Should().BeOfType<HttpClient>();
}
[Fact]
public void CreateSessionWithToken_ReplacesExistingSession()
{
// Arrange
SessionManager.CreateSessionWithToken("https://old.com", "oldApp", "oldToken");
// Act
var newSession = SessionManager.CreateSessionWithToken("https://new.com", "newApp", "newToken");
// Assert
SessionManager.CurrentSession.Should().BeSameAs(newSession);
SessionManager.CurrentSession!.URL.Should().Be("https://new.com");
SessionManager.CurrentSession.AppID.Should().Be("newApp");
SessionManager.CurrentSession.Token.Should().Be("newToken");
}
}

View File

@@ -0,0 +1,139 @@
namespace PS.IPAM.Tests.Mocks;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Reflection;
/// <summary>
/// Helper class for testing PowerShell cmdlets.
/// </summary>
public class CmdletTestHelper<T> : IDisposable where T : PSCmdlet, new()
{
private readonly T _cmdlet;
private readonly List<object> _output = new();
private readonly List<ErrorRecord> _errors = new();
private bool _disposed;
public CmdletTestHelper()
{
_cmdlet = new T();
}
/// <summary>
/// Gets the cmdlet instance for setting parameters.
/// </summary>
public T Cmdlet => _cmdlet;
/// <summary>
/// Gets the output objects written by the cmdlet.
/// </summary>
public IReadOnlyList<object> Output => _output.AsReadOnly();
/// <summary>
/// Gets the errors written by the cmdlet.
/// </summary>
public IReadOnlyList<ErrorRecord> Errors => _errors.AsReadOnly();
/// <summary>
/// Gets whether any errors were written.
/// </summary>
public bool HasErrors => _errors.Count > 0;
/// <summary>
/// Invokes the cmdlet and captures output.
/// </summary>
public void Invoke()
{
// Use reflection to call the protected ProcessRecord method
var processMethod = typeof(T).GetMethod("ProcessRecord",
BindingFlags.NonPublic | BindingFlags.Instance);
if (processMethod == null)
{
throw new InvalidOperationException("ProcessRecord method not found on cmdlet.");
}
// Set up a mock command runtime to capture output
var runtime = new MockCommandRuntime(_output, _errors);
var runtimeProperty = typeof(Cmdlet).GetProperty("CommandRuntime",
BindingFlags.Public | BindingFlags.Instance);
runtimeProperty?.SetValue(_cmdlet, runtime);
// Invoke the method
try
{
processMethod.Invoke(_cmdlet, null);
}
catch (TargetInvocationException ex) when (ex.InnerException != null)
{
// Unwrap the exception
throw ex.InnerException;
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
}
}
}
/// <summary>
/// Mock implementation of ICommandRuntime for capturing cmdlet output.
/// This is a minimal implementation that only handles the methods we need for testing.
/// </summary>
internal class MockCommandRuntime : ICommandRuntime
{
private readonly List<object> _output;
private readonly List<ErrorRecord> _errors;
public MockCommandRuntime(List<object> output, List<ErrorRecord> errors)
{
_output = output;
_errors = errors;
}
public PSHost Host => null!;
public PSTransactionContext CurrentPSTransaction => null!;
public bool ShouldContinue(string query, string caption) => true;
public bool ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll) => true;
public bool ShouldProcess(string target) => true;
public bool ShouldProcess(string target, string action) => true;
public bool ShouldProcess(string verboseDescription, string verboseWarning, string caption) => true;
public bool ShouldProcess(string verboseDescription, string verboseWarning, string caption, out ShouldProcessReason shouldProcessReason)
{
shouldProcessReason = ShouldProcessReason.None;
return true;
}
public bool TransactionAvailable() => false;
public void ThrowTerminatingError(ErrorRecord errorRecord) => throw errorRecord.Exception;
public void WriteCommandDetail(string text) { }
public void WriteDebug(string text) { }
public void WriteError(ErrorRecord errorRecord) => _errors.Add(errorRecord);
public void WriteObject(object sendToPipeline) => _output.Add(sendToPipeline);
public void WriteObject(object sendToPipeline, bool enumerateCollection)
{
if (enumerateCollection && sendToPipeline is System.Collections.IEnumerable enumerable)
{
foreach (var item in enumerable)
{
_output.Add(item);
}
}
else
{
_output.Add(sendToPipeline);
}
}
public void WriteProgress(ProgressRecord progressRecord) { }
public void WriteProgress(long sourceId, ProgressRecord progressRecord) { }
public void WriteVerbose(string text) { }
public void WriteWarning(string text) { }
}

View File

@@ -0,0 +1,172 @@
namespace PS.IPAM.Tests.Mocks;
using System.Net;
/// <summary>
/// A mock HTTP message handler for testing HTTP requests without making actual network calls.
/// This handler does not dispose itself when the HttpClient is disposed, allowing reuse in tests.
/// </summary>
public class MockHttpMessageHandler : HttpMessageHandler
{
private readonly Queue<MockResponse> _responses = new();
private readonly List<HttpRequestMessage> _requests = new();
private bool _disposed = false;
/// <summary>
/// Gets all requests that were sent through this handler.
/// </summary>
public IReadOnlyList<HttpRequestMessage> Requests => _requests.AsReadOnly();
/// <summary>
/// Gets the last request that was sent through this handler.
/// </summary>
public HttpRequestMessage? LastRequest => _requests.LastOrDefault();
/// <summary>
/// Queues a response to be returned for the next request.
/// </summary>
public MockHttpMessageHandler WithResponse(HttpStatusCode statusCode, string content, string contentType = "application/json")
{
_responses.Enqueue(new MockResponse(statusCode, content, contentType));
return this;
}
/// <summary>
/// Queues a successful JSON response.
/// </summary>
public MockHttpMessageHandler WithJsonResponse(string jsonContent)
{
return WithResponse(HttpStatusCode.OK, jsonContent, "application/json");
}
/// <summary>
/// Queues a successful response with phpIPAM-style wrapper.
/// </summary>
public MockHttpMessageHandler WithSuccessResponse(string dataJson)
{
var response = $"{{\"code\":200,\"success\":true,\"data\":{dataJson}}}";
return WithJsonResponse(response);
}
/// <summary>
/// Queues a 404 Not Found response.
/// </summary>
public MockHttpMessageHandler WithNotFoundResponse()
{
return WithResponse(HttpStatusCode.NotFound, "{\"code\":404,\"success\":false,\"message\":\"Not found\"}", "application/json");
}
/// <summary>
/// Queues an error response.
/// </summary>
public MockHttpMessageHandler WithErrorResponse(HttpStatusCode statusCode, string message)
{
var response = $"{{\"code\":{(int)statusCode},\"success\":false,\"message\":\"{message}\"}}";
return WithResponse(statusCode, response, "application/json");
}
/// <summary>
/// Queues an exception to be thrown on the next request.
/// </summary>
public MockHttpMessageHandler WithException(Exception exception)
{
_responses.Enqueue(new MockResponse(exception));
return this;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MockHttpMessageHandler));
}
_requests.Add(request);
if (_responses.Count == 0)
{
throw new InvalidOperationException("No mock response configured. Call WithResponse() before making requests.");
}
var mockResponse = _responses.Dequeue();
if (mockResponse.Exception != null)
{
throw mockResponse.Exception;
}
var response = new HttpResponseMessage(mockResponse.StatusCode)
{
Content = new StringContent(mockResponse.Content, System.Text.Encoding.UTF8, mockResponse.ContentType),
RequestMessage = request
};
return Task.FromResult(response);
}
protected override void Dispose(bool disposing)
{
// Don't actually dispose - allow reuse in tests
// The test itself is responsible for cleanup
}
/// <summary>
/// Actually disposes the handler. Call this in test cleanup.
/// </summary>
public void ForceDispose()
{
_disposed = true;
base.Dispose(true);
}
/// <summary>
/// Verifies that a request was made to the expected URL.
/// </summary>
public bool WasRequestMadeTo(string urlContains)
{
return _requests.Any(r => r.RequestUri?.ToString().Contains(urlContains) == true);
}
/// <summary>
/// Verifies that a request with the expected method was made.
/// </summary>
public bool WasRequestMadeWithMethod(HttpMethod method)
{
return _requests.Any(r => r.Method == method);
}
/// <summary>
/// Gets the value of a header from the last request.
/// </summary>
public string? GetLastRequestHeader(string headerName)
{
if (LastRequest?.Headers.TryGetValues(headerName, out var values) == true)
{
return values.FirstOrDefault();
}
return null;
}
private class MockResponse
{
public HttpStatusCode StatusCode { get; }
public string Content { get; }
public string ContentType { get; }
public Exception? Exception { get; }
public MockResponse(HttpStatusCode statusCode, string content, string contentType)
{
StatusCode = statusCode;
Content = content;
ContentType = contentType;
}
public MockResponse(Exception exception)
{
Exception = exception;
StatusCode = HttpStatusCode.InternalServerError;
Content = string.Empty;
ContentType = string.Empty;
}
}
}

View File

@@ -0,0 +1,136 @@
namespace PS.IPAM.Tests.Models;
using FluentAssertions;
using PS.IPAM;
using Xunit;
public class AddressTests
{
[Fact]
public void Constructor_SetsAllProperties()
{
// Arrange
var id = 1;
var subnetId = 10;
var ip = "192.168.1.100";
var isGateway = true;
var description = "Test server";
var hostname = "server01.example.com";
var mac = "00:11:22:33:44:55";
var owner = "admin";
var tagId = 2;
var deviceId = 5;
var location = "DC1";
var port = "eth0";
var note = "Production server";
var lastSeen = new DateTime(2026, 1, 15, 10, 30, 0);
var excludePing = false;
var ptrIgnore = true;
var ptr = 1;
var firewallObject = "FW_SERVER01";
var editDate = new DateTime(2026, 1, 10, 8, 0, 0);
var customerId = 100;
var extendedData = new Dictionary<string, object> { { "custom_field1", "value1" } };
// Act
var address = new Address(
id, subnetId, ip, isGateway, description, hostname, mac, owner,
tagId, deviceId, location, port, note, lastSeen, excludePing,
ptrIgnore, ptr, firewallObject, editDate, customerId, extendedData
);
// Assert
address.Id.Should().Be(id);
address.SubnetId.Should().Be(subnetId);
address.Ip.Should().Be(ip);
address.IsGateway.Should().Be(isGateway);
address.Description.Should().Be(description);
address.Hostname.Should().Be(hostname);
address.MAC.Should().Be(mac);
address.Owner.Should().Be(owner);
address.TagId.Should().Be(tagId);
address.DeviceId.Should().Be(deviceId);
address.Location.Should().Be(location);
address.Port.Should().Be(port);
address.Note.Should().Be(note);
address.LastSeen.Should().Be(lastSeen);
address.ExcludePing.Should().Be(excludePing);
address.PTRIgnore.Should().Be(ptrIgnore);
address.PTR.Should().Be(ptr);
address.FirewallAddressObject.Should().Be(firewallObject);
address.EditDate.Should().Be(editDate);
address.CustomerId.Should().Be(customerId);
address.ExtendedData.Should().BeEquivalentTo(extendedData);
}
[Fact]
public void Constructor_WithNullOptionalFields_SetsNullValues()
{
// Act
var address = new Address(
1, 10, "10.0.0.1", false, "", "", "", "",
0, 0, "", "", "", null, false,
false, 0, "", null, 0, null
);
// Assert
address.LastSeen.Should().BeNull();
address.EditDate.Should().BeNull();
address.ExtendedData.Should().BeNull();
}
[Fact]
public void ToString_ReturnsIpAddress()
{
// Arrange
var address = new Address(
1, 10, "192.168.1.50", false, "Test", "host.local", "", "",
0, 0, "", "", "", null, false,
false, 0, "", null, 0, null
);
// Act
var result = address.ToString();
// Assert
result.Should().Be("192.168.1.50");
}
[Theory]
[InlineData("10.0.0.1")]
[InlineData("172.16.0.100")]
[InlineData("192.168.255.255")]
[InlineData("2001:db8::1")]
public void ToString_ReturnsCorrectIp_ForVariousAddresses(string expectedIp)
{
// Arrange
var address = new Address(
1, 1, expectedIp, false, "", "", "", "",
0, 0, "", "", "", null, false,
false, 0, "", null, 0, null
);
// Act & Assert
address.ToString().Should().Be(expectedIp);
}
[Fact]
public void Record_Equality_WorksCorrectly()
{
// Arrange
var address1 = new Address(
1, 10, "192.168.1.1", false, "Test", "host", "", "",
0, 0, "", "", "", null, false,
false, 0, "", null, 0, null
);
var address2 = new Address(
1, 10, "192.168.1.1", false, "Test", "host", "", "",
0, 0, "", "", "", null, false,
false, 0, "", null, 0, null
);
// Assert
address1.Should().Be(address2);
}
}

View File

@@ -0,0 +1,43 @@
namespace PS.IPAM.Tests.Models;
using FluentAssertions;
using PS.IPAM;
using Xunit;
public class DomainTests
{
[Fact]
public void Constructor_SetsAllProperties()
{
// Arrange
var id = 1;
var name = "Default";
var description = "Default L2 domain";
var sections = "1;2;3";
// Act
var domain = new Domain(id, name, description, sections);
// Assert
domain.Id.Should().Be(id);
domain.Name.Should().Be(name);
domain.Description.Should().Be(description);
domain.Sections.Should().Be(sections);
}
[Theory]
[InlineData("Default")]
[InlineData("Datacenter1")]
[InlineData("Branch_Office")]
public void ToString_ReturnsDomainName(string domainName)
{
// Arrange
var domain = new Domain(1, domainName, "", "");
// Act
var result = domain.ToString();
// Assert
result.Should().Be(domainName);
}
}

View File

@@ -0,0 +1,104 @@
namespace PS.IPAM.Tests.Models;
using FluentAssertions;
using PS.IPAM;
using Xunit;
public class NameserverTests
{
[Fact]
public void Constructor_SetsAllProperties()
{
// Arrange
var id = 1;
var name = "Google DNS";
var nameServers = "8.8.8.8;8.8.4.4";
var description = "Google Public DNS";
var permissions = "{\"3\":\"2\"}";
var editDate = new DateTime(2026, 1, 10);
// Act
var nameserver = new Nameserver(id, name, nameServers, description, permissions, editDate);
// Assert
nameserver.Id.Should().Be(id);
nameserver.Name.Should().Be(name);
nameserver.Description.Should().Be(description);
nameserver.Permissions.Should().Be(permissions);
nameserver.EditDate.Should().Be(editDate);
}
[Fact]
public void Constructor_ParsesNameservers_BySemicolon()
{
// Arrange
var nameServersString = "8.8.8.8;8.8.4.4;1.1.1.1";
// Act
var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null);
// Assert
nameserver.NameServers.Should().HaveCount(3);
nameserver.NameServers.Should().ContainInOrder("8.8.8.8", "8.8.4.4", "1.1.1.1");
}
[Fact]
public void Constructor_WithSingleNameserver_ReturnsArrayWithOneElement()
{
// Arrange
var nameServersString = "8.8.8.8";
// Act
var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null);
// Assert
nameserver.NameServers.Should().HaveCount(1);
nameserver.NameServers[0].Should().Be("8.8.8.8");
}
[Fact]
public void Constructor_WithEmptyNameservers_ReturnsEmptyArray()
{
// Arrange
var nameServersString = "";
// Act
var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null);
// Assert - Empty entries are excluded
nameserver.NameServers.Should().BeEmpty();
}
[Fact]
public void Constructor_WithNullEditDate_SetsNull()
{
// Act
var nameserver = new Nameserver(1, "Test", "8.8.8.8", "", "", null);
// Assert
nameserver.EditDate.Should().BeNull();
}
[Fact]
public void Constructor_WithSemicolonOnlyString_ReturnsEmptyArray()
{
// Arrange - edge case with just semicolons
var nameServersString = ";;;";
// Act
var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null);
// Assert
nameserver.NameServers.Should().BeEmpty();
}
[Fact]
public void ToString_ReturnsName()
{
// Arrange
var nameserver = new Nameserver(1, "Google DNS", "8.8.8.8", "", "", null);
// Act & Assert
nameserver.ToString().Should().Be("Google DNS");
}
}

View File

@@ -0,0 +1,77 @@
namespace PS.IPAM.Tests.Models;
using FluentAssertions;
using PS.IPAM;
using Xunit;
public class SectionTests
{
[Fact]
public void Constructor_SetsAllProperties()
{
// Arrange
var id = 1;
var name = "Production";
var description = "Production networks section";
var masterSectionId = 0;
var permissions = "{\"3\":\"2\"}";
var strictMode = true;
var subnetOrdering = "subnet,asc";
var order = 1;
var editDate = new DateTime(2026, 1, 5);
var showSubnet = true;
var showVlan = true;
var showVRF = false;
var showSupernetOnly = false;
var dnsId = 1;
// Act
var section = new Section(
id, name, description, masterSectionId, permissions, strictMode,
subnetOrdering, order, editDate, showSubnet, showVlan, showVRF,
showSupernetOnly, dnsId
);
// Assert
section.Id.Should().Be(id);
section.Name.Should().Be(name);
section.Description.Should().Be(description);
section.MasterSectionId.Should().Be(masterSectionId);
section.Permissions.Should().Be(permissions);
section.StrictMode.Should().Be(strictMode);
section.SubnetOrdering.Should().Be(subnetOrdering);
section.Order.Should().Be(order);
section.EditDate.Should().Be(editDate);
section.ShowSubnet.Should().Be(showSubnet);
section.ShowVlan.Should().Be(showVlan);
section.ShowVRF.Should().Be(showVRF);
section.ShowSupernetOnly.Should().Be(showSupernetOnly);
section.DNSId.Should().Be(dnsId);
}
[Theory]
[InlineData("Production")]
[InlineData("Development")]
[InlineData("DMZ")]
public void ToString_ReturnsSectionName(string sectionName)
{
// Arrange
var section = new Section(1, sectionName, "", 0, "", false, "", 0, null, false, false, false, false, 0);
// Act
var result = section.ToString();
// Assert
result.Should().Be(sectionName);
}
[Fact]
public void Constructor_WithNullEditDate_SetsNull()
{
// Act
var section = new Section(1, "Test", "", 0, "", false, "", 0, null, false, false, false, false, 0);
// Assert
section.EditDate.Should().BeNull();
}
}

View File

@@ -0,0 +1,90 @@
namespace PS.IPAM.Tests.Models;
using FluentAssertions;
using PS.IPAM;
using Xunit;
public class SessionTests
{
[Fact]
public void Constructor_WithCredentialsAuth_SetsAllProperties()
{
// Arrange
var authType = AuthType.Credentials;
var token = "test-token-123";
var appId = "myApp";
var url = "https://ipam.example.com";
var expires = new DateTime(2026, 12, 31, 23, 59, 59);
var credentials = new object(); // Mock credentials
// Act
var session = new Session(authType, token, appId, url, expires, credentials);
// Assert
session.AuthType.Should().Be(AuthType.Credentials);
session.Token.Should().Be(token);
session.AppID.Should().Be(appId);
session.URL.Should().Be(url);
session.Expires.Should().Be(expires);
session.Credentials.Should().BeSameAs(credentials);
}
[Fact]
public void Constructor_WithTokenAuth_SetsAllProperties()
{
// Arrange
var authType = AuthType.Token;
var token = "static-api-token";
var appId = "apiApp";
var url = "https://ipam.test.com";
// Act
var session = new Session(authType, token, appId, url, null, null);
// Assert
session.AuthType.Should().Be(AuthType.Token);
session.Token.Should().Be(token);
session.AppID.Should().Be(appId);
session.URL.Should().Be(url);
session.Expires.Should().BeNull();
session.Credentials.Should().BeNull();
}
[Fact]
public void Token_CanBeModified()
{
// Arrange
var session = new Session(AuthType.Credentials, "old-token", "app", "https://test.com", null, null);
// Act
session.Token = "new-token";
// Assert
session.Token.Should().Be("new-token");
}
[Fact]
public void Expires_CanBeModified()
{
// Arrange
var session = new Session(AuthType.Credentials, "token", "app", "https://test.com", null, null);
var newExpiry = new DateTime(2027, 1, 1);
// Act
session.Expires = newExpiry;
// Assert
session.Expires.Should().Be(newExpiry);
}
[Fact]
public void Record_Equality_WorksForSameValues()
{
// Arrange
var session1 = new Session(AuthType.Token, "token", "app", "https://test.com", null, null);
var session2 = new Session(AuthType.Token, "token", "app", "https://test.com", null, null);
// Assert - Records use value equality
session1.Should().Be(session2);
}
}

View File

@@ -0,0 +1,157 @@
namespace PS.IPAM.Tests.Models;
using FluentAssertions;
using PS.IPAM;
using Xunit;
public class SubnetworkTests
{
[Fact]
public void Constructor_SetsAllProperties()
{
// Arrange
var id = 1;
var subnet = "192.168.1.0";
var mask = 24;
var sectionId = 5;
var description = "Production network";
var linkedSubnet = "linked-123";
var firewallObject = "FW_PROD";
var vrfId = 2;
var masterSubnetId = 0;
var allowRequests = true;
var vlanId = 100;
var showName = true;
var deviceId = 10;
var permissions = "rw";
var pingSubnet = true;
var discoverSubnet = false;
var resolveDNS = true;
var dnsRecursive = false;
var dnsRecords = true;
var nameserverId = 3;
var scanAgent = false;
var isFolder = false;
var isFull = false;
var isPool = true;
var tagId = 1;
var threshold = 80;
var locationId = 4;
var editDate = new DateTime(2026, 1, 10);
var lastScan = new DateTime(2026, 1, 9);
var lastDiscovery = new DateTime(2026, 1, 8);
var calculation = new { maxhosts = 254 };
var customFields = new Dictionary<string, object> { { "custom_env", "prod" } };
// Act
var subnetwork = new Subnetwork(
id, subnet, mask, sectionId, description, linkedSubnet, firewallObject,
vrfId, masterSubnetId, allowRequests, vlanId, showName, deviceId,
permissions, pingSubnet, discoverSubnet, resolveDNS, dnsRecursive,
dnsRecords, nameserverId, scanAgent, isFolder, isFull, isPool,
tagId, threshold, locationId, editDate, lastScan, lastDiscovery,
calculation, customFields
);
// Assert
subnetwork.Id.Should().Be(id);
subnetwork.Subnet.Should().Be(subnet);
subnetwork.Mask.Should().Be(mask);
subnetwork.SectionId.Should().Be(sectionId);
subnetwork.Description.Should().Be(description);
subnetwork.LinkedSubnet.Should().Be(linkedSubnet);
subnetwork.FirewallAddressObject.Should().Be(firewallObject);
subnetwork.VrfId.Should().Be(vrfId);
subnetwork.MasterSubnetId.Should().Be(masterSubnetId);
subnetwork.AllowRequests.Should().Be(allowRequests);
subnetwork.VlanId.Should().Be(vlanId);
subnetwork.ShowName.Should().Be(showName);
subnetwork.DeviceId.Should().Be(deviceId);
subnetwork.Permissions.Should().Be(permissions);
subnetwork.PingSubnet.Should().Be(pingSubnet);
subnetwork.DiscoverSubnet.Should().Be(discoverSubnet);
subnetwork.ResolveDNS.Should().Be(resolveDNS);
subnetwork.DNSRecursive.Should().Be(dnsRecursive);
subnetwork.DNSRecords.Should().Be(dnsRecords);
subnetwork.NameserverId.Should().Be(nameserverId);
subnetwork.ScanAgent.Should().Be(scanAgent);
subnetwork.IsFolder.Should().Be(isFolder);
subnetwork.IsFull.Should().Be(isFull);
subnetwork.IsPool.Should().Be(isPool);
subnetwork.TagId.Should().Be(tagId);
subnetwork.Threshold.Should().Be(threshold);
subnetwork.LocationId.Should().Be(locationId);
subnetwork.EditDate.Should().Be(editDate);
subnetwork.LastScan.Should().Be(lastScan);
subnetwork.LastDiscovery.Should().Be(lastDiscovery);
subnetwork.Calculation.Should().BeEquivalentTo(calculation);
subnetwork.ExtendedData.Should().BeEquivalentTo(customFields);
}
[Theory]
[InlineData("192.168.1.0", 24, "192.168.1.0/24")]
[InlineData("10.0.0.0", 8, "10.0.0.0/8")]
[InlineData("172.16.0.0", 16, "172.16.0.0/16")]
[InlineData("192.168.100.0", 30, "192.168.100.0/30")]
public void CIDR_ReturnsCidrNotation(string subnet, int mask, string expectedCidr)
{
// Arrange
var subnetwork = CreateSubnetwork(subnet, mask);
// Act
var result = subnetwork.CIDR;
// Assert
result.Should().Be(expectedCidr);
}
[Fact]
public void ToString_ReturnsCidrNotation()
{
// Arrange
var subnetwork = CreateSubnetwork("10.10.0.0", 16);
// Act
var result = subnetwork.ToString();
// Assert
result.Should().Be("10.10.0.0/16");
}
[Fact]
public void ToString_EqualsCIDRProperty()
{
// Arrange
var subnetwork = CreateSubnetwork("172.20.0.0", 12);
// Act & Assert
subnetwork.ToString().Should().Be(subnetwork.CIDR);
}
[Fact]
public void Record_Equality_WorksCorrectly()
{
// Arrange - use same calculation object for equality comparison
var calculation = new { maxhosts = 254 };
var subnet1 = CreateSubnetwork("192.168.1.0", 24, calculation);
var subnet2 = CreateSubnetwork("192.168.1.0", 24, calculation);
// Assert - records use value equality (except for the Calculation object reference)
subnet1.Id.Should().Be(subnet2.Id);
subnet1.Subnet.Should().Be(subnet2.Subnet);
subnet1.Mask.Should().Be(subnet2.Mask);
subnet1.CIDR.Should().Be(subnet2.CIDR);
}
private static Subnetwork CreateSubnetwork(string subnet, int mask, object? calculation = null)
{
return new Subnetwork(
1, subnet, mask, 1, "", "", "",
0, 0, false, 0, false, 0,
"", false, false, false, false,
false, 0, false, false, false, false,
0, 0, 0, null, null, null,
calculation ?? new object(), null
);
}
}

View File

@@ -0,0 +1,63 @@
namespace PS.IPAM.Tests.Models;
using FluentAssertions;
using PS.IPAM;
using Xunit;
public class TagTests
{
[Fact]
public void Constructor_SetsAllProperties()
{
// Arrange
var id = 1;
var type = "Used";
var showTag = true;
var bgColor = "#5cb85c";
var fgColor = "#ffffff";
var compress = "Yes";
var locked = "No";
var updateTag = true;
// Act
var tag = new Tag(id, type, showTag, bgColor, fgColor, compress, locked, updateTag);
// Assert
tag.Id.Should().Be(id);
tag.Type.Should().Be(type);
tag.ShowTag.Should().Be(showTag);
tag.BackgroundColor.Should().Be(bgColor);
tag.ForegroundColor.Should().Be(fgColor);
tag.Compress.Should().Be(compress);
tag.Locked.Should().Be(locked);
tag.UpdateTag.Should().Be(updateTag);
}
[Theory]
[InlineData("Used")]
[InlineData("Available")]
[InlineData("Reserved")]
[InlineData("DHCP")]
public void ToString_ReturnsTagType(string tagType)
{
// Arrange
var tag = new Tag(1, tagType, false, "", "", "No", "No", false);
// Act
var result = tag.ToString();
// Assert
result.Should().Be(tagType);
}
[Fact]
public void Record_Equality_WorksCorrectly()
{
// Arrange
var tag1 = new Tag(1, "Used", true, "#fff", "#000", "Yes", "No", false);
var tag2 = new Tag(1, "Used", true, "#fff", "#000", "Yes", "No", false);
// Assert
tag1.Should().Be(tag2);
}
}

View File

@@ -0,0 +1,76 @@
namespace PS.IPAM.Tests.Models;
using FluentAssertions;
using PS.IPAM;
using Xunit;
public class VlanTests
{
[Fact]
public void Constructor_SetsAllProperties()
{
// Arrange
var vlanId = 100;
var domainId = 1;
var name = "Production VLAN";
var number = 100;
var description = "Production network VLAN";
var editDate = new DateTime(2026, 1, 15);
var customerId = 50;
var customFields = new Dictionary<string, object> { { "custom_location", "DC1" } };
// Act
var vlan = new Vlan(vlanId, domainId, name, number, description, editDate, customerId, customFields);
// Assert
vlan.Id.Should().Be(vlanId);
vlan.VlanId.Should().Be(vlanId);
vlan.DomainId.Should().Be(domainId);
vlan.Name.Should().Be(name);
vlan.Number.Should().Be(number);
vlan.Description.Should().Be(description);
vlan.EditDate.Should().Be(editDate);
vlan.CustomerId.Should().Be(customerId);
vlan.ExtendedData.Should().BeEquivalentTo(customFields);
}
[Fact]
public void Id_And_VlanId_AreSameValue()
{
// Arrange
var vlanId = 200;
// Act
var vlan = new Vlan(vlanId, 1, "Test", 200, "", null, 0, null);
// Assert
vlan.Id.Should().Be(vlan.VlanId);
}
[Theory]
[InlineData(1)]
[InlineData(100)]
[InlineData(4094)]
public void ToString_ReturnsVlanNumber(int vlanNumber)
{
// Arrange
var vlan = new Vlan(1, 1, "Test", vlanNumber, "", null, 0, null);
// Act
var result = vlan.ToString();
// Assert
result.Should().Be(vlanNumber.ToString());
}
[Fact]
public void Constructor_WithNullOptionalFields_SetsNullValues()
{
// Act
var vlan = new Vlan(1, 1, "Test", 10, "", null, 0, null);
// Assert
vlan.EditDate.Should().BeNull();
vlan.ExtendedData.Should().BeNull();
}
}

View File

@@ -0,0 +1,60 @@
namespace PS.IPAM.Tests.Models;
using FluentAssertions;
using PS.IPAM;
using Xunit;
public class VrfTests
{
[Fact]
public void Constructor_SetsAllProperties()
{
// Arrange
var id = 1;
var name = "VRF_PROD";
var rd = "65000:100";
var description = "Production VRF";
var sections = "1;2;3";
var editDate = new DateTime(2026, 1, 10);
var customFields = new Dictionary<string, object> { { "custom_tenant", "CustomerA" } };
// Act
var vrf = new Vrf(id, name, rd, description, sections, editDate, customFields);
// Assert
vrf.Id.Should().Be(id);
vrf.Name.Should().Be(name);
vrf.RouteDistinguisher.Should().Be(rd);
vrf.Description.Should().Be(description);
vrf.Sections.Should().Be(sections);
vrf.EditDate.Should().Be(editDate);
vrf.ExtendedData.Should().BeEquivalentTo(customFields);
}
[Theory]
[InlineData("VRF_DEFAULT")]
[InlineData("Production_VRF")]
[InlineData("CUSTOMER_A")]
public void ToString_ReturnsVrfName(string vrfName)
{
// Arrange
var vrf = new Vrf(1, vrfName, "65000:1", "", "", null, null);
// Act
var result = vrf.ToString();
// Assert
result.Should().Be(vrfName);
}
[Fact]
public void Constructor_WithNullOptionalFields_SetsNullValues()
{
// Act
var vrf = new Vrf(1, "Test", "65000:1", "", "", null, null);
// Assert
vrf.EditDate.Should().BeNull();
vrf.ExtendedData.Should().BeNull();
}
}

View File

@@ -0,0 +1,32 @@
namespace PS.IPAM.Tests;
using Xunit;
/// <summary>
/// Collection definition for tests that share static state (SessionManager, RequestHelper.TestHttpHandler).
/// Tests in this collection will run sequentially, not in parallel.
/// </summary>
[CollectionDefinition("Sequential")]
public class SequentialCollection : ICollectionFixture<SequentialTestFixture>
{
}
/// <summary>
/// Fixture for sequential test collection.
/// </summary>
public class SequentialTestFixture : IDisposable
{
public SequentialTestFixture()
{
// Clean up before tests
PS.IPAM.Helpers.SessionManager.CloseSession();
PS.IPAM.Helpers.RequestHelper.TestHttpHandler = null;
}
public void Dispose()
{
// Clean up after tests
PS.IPAM.Helpers.SessionManager.CloseSession();
PS.IPAM.Helpers.RequestHelper.TestHttpHandler = null;
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>PS.IPAM.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\classlib\classlib.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,60 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Assigns a tag to an address in phpIPAM.
/// </summary>
[Cmdlet("Assign", "Tag", SupportsShouldProcess = true)]
[OutputType(typeof(Address))]
public class AssignTagCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
HelpMessage = "The address to assign the tag to.")]
[ValidateNotNullOrEmpty]
public Address? AddressObject { get; set; }
[Parameter(
Mandatory = true,
Position = 1,
HelpMessage = "The tag to assign.")]
[ValidateNotNullOrEmpty]
public Tag? Tag { get; set; }
protected override void ProcessRecord()
{
try
{
var identifiers = new[] { AddressObject!.Id.ToString() };
var body = new Dictionary<string, object>
{
["tag"] = Tag!.Id
};
if (ShouldProcess($"Address {AddressObject.Ip}", $"Assign tag '{Tag.Type}'"))
{
RequestHelper.InvokeRequest(
"PATCH", ApiController.Addresses, null, null, body, identifiers
).GetAwaiter().GetResult();
// Return the updated address
var address = RequestHelper.InvokeRequest(
"GET", ApiController.Addresses, ModelType.Address, null, null, identifiers
).GetAwaiter().GetResult();
WriteResult(address);
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "AssignTagError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,114 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Base class for all PS.IPAM cmdlets providing common functionality.
/// </summary>
public abstract class BaseCmdlet : PSCmdlet
{
/// <summary>
/// Writes the result to the pipeline, handling both single objects and collections.
/// </summary>
/// <param name="result">The result to write to the pipeline.</param>
protected void WriteResult(object? result)
{
if (result == null)
{
return;
}
if (result is IEnumerable enumerable && result is not string)
{
foreach (var item in enumerable)
{
WriteObject(item);
}
}
else
{
WriteObject(result);
}
}
/// <summary>
/// Converts custom fields from various formats to a dictionary.
/// </summary>
/// <param name="customFields">The custom fields object to convert.</param>
/// <returns>A dictionary of custom field names and values.</returns>
protected static Dictionary<string, object> ConvertCustomFields(object? customFields)
{
if (customFields == null)
{
return new Dictionary<string, object>();
}
if (customFields is PSObject psobj)
{
var dict = new Dictionary<string, object>();
foreach (var prop in psobj.Properties)
{
dict[prop.Name] = prop.Value ?? string.Empty;
}
return dict;
}
if (customFields is Dictionary<string, object> dictObj)
{
return dictObj;
}
if (customFields is IDictionary genericDict)
{
var result = new Dictionary<string, object>();
foreach (DictionaryEntry entry in genericDict)
{
result[entry.Key?.ToString() ?? string.Empty] = entry.Value ?? string.Empty;
}
return result;
}
return new Dictionary<string, object>();
}
/// <summary>
/// Executes an async operation synchronously and handles errors.
/// </summary>
/// <typeparam name="T">The return type of the operation.</typeparam>
/// <param name="operation">The async operation to execute.</param>
/// <param name="errorId">The error ID for error reporting.</param>
/// <returns>The result of the operation, or default if an error occurred.</returns>
protected T? ExecuteAsync<T>(Func<System.Threading.Tasks.Task<T>> operation, string errorId)
{
try
{
return operation().GetAwaiter().GetResult();
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, errorId, ErrorCategory.InvalidOperation, null));
return default;
}
}
/// <summary>
/// Executes an async operation synchronously and handles errors.
/// </summary>
/// <param name="operation">The async operation to execute.</param>
/// <param name="errorId">The error ID for error reporting.</param>
protected void ExecuteAsync(Func<System.Threading.Tasks.Task> operation, string errorId)
{
try
{
operation().GetAwaiter().GetResult();
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, errorId, ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,17 @@
namespace PS.IPAM.Cmdlets;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Closes the current phpIPAM API session.
/// </summary>
[Cmdlet(VerbsCommon.Close, "Session")]
public class CloseSessionCmdlet : BaseCmdlet
{
protected override void ProcessRecord()
{
SessionManager.CloseSession();
WriteVerbose("Session closed successfully.");
}
}

View File

@@ -0,0 +1,169 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Net;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves IP address entries from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "Address", DefaultParameterSetName = "ByID")]
[OutputType(typeof(Address))]
public class GetAddressCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The address ID to retrieve.")]
[ValidateNotNullOrEmpty]
public int Id { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByIP",
HelpMessage = "The IP address to search for.")]
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 1,
ParameterSetName = "BySubnetId")]
[ValidateNotNullOrEmpty]
public IPAddress? IP { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByHostName",
HelpMessage = "The hostname to search for.")]
[ValidateNotNullOrEmpty]
public string? HostName { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByHostBase",
HelpMessage = "The hostname base pattern to search for.")]
[ValidateNotNullOrEmpty]
public string? HostBase { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByTag",
HelpMessage = "The tag ID to filter addresses by.")]
[ValidateNotNullOrEmpty]
public int? TagId { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySubnetId",
HelpMessage = "The subnet ID to get addresses from.")]
[ValidateNotNullOrEmpty]
public int? SubnetId { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySubnetCIDR",
HelpMessage = "The subnet in CIDR notation to get addresses from.")]
[ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")]
[ValidateNotNullOrEmpty]
public string? SubnetCIDR { get; set; }
protected override void ProcessRecord()
{
try
{
var controller = ApiController.Addresses;
var identifiers = new List<string>();
switch (ParameterSetName)
{
case "ByID":
identifiers.Add(Id.ToString());
break;
case "ByIP":
identifiers.Add("search");
identifiers.Add(IP!.ToString());
break;
case "ByHostName":
identifiers.Add("search_hostname");
identifiers.Add(HostName!);
break;
case "ByHostBase":
identifiers.Add("search_hostbase");
identifiers.Add(HostBase!);
break;
case "ByTag":
identifiers.Add("tags");
identifiers.Add(TagId!.Value.ToString());
identifiers.Add("addresses");
break;
case "BySubnetId":
if (IP != null)
{
identifiers.Add(IP.ToString());
identifiers.Add(SubnetId!.Value.ToString());
}
else
{
controller = ApiController.Subnets;
identifiers.Add(SubnetId!.Value.ToString());
identifiers.Add("addresses");
}
break;
case "BySubnetCIDR":
controller = ApiController.Subnets;
var subnet = RequestHelper.InvokeRequest(
"GET", ApiController.Subnets, ModelType.Subnetwork, null, null,
new[] { "cidr", SubnetCIDR! }
).GetAwaiter().GetResult() as Subnetwork;
if (subnet == null)
{
throw new ItemNotFoundException($"Subnet '{SubnetCIDR}' not found.");
}
identifiers.Add(subnet.Id.ToString());
identifiers.Add("addresses");
break;
}
var result = RequestHelper.InvokeRequest(
"GET", controller, ModelType.Address, null, null, identifiers.ToArray()
).GetAwaiter().GetResult();
WriteResult(result);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetAddressError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,90 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Gets the first available IP address in a subnet.
/// </summary>
[Cmdlet(VerbsCommon.Get, "FirstFreeIP", DefaultParameterSetName = "ByID")]
public class GetFirstFreeIPCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByCIDR",
HelpMessage = "The subnet in CIDR notation.")]
[ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")]
[ValidateNotNullOrEmpty]
public string? CIDR { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The subnet ID.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySubnetObject",
HelpMessage = "The subnet object.")]
[ValidateNotNullOrEmpty]
public Subnetwork? SubnetObject { get; set; }
protected override void ProcessRecord()
{
try
{
var subnetId = GetSubnetId();
var identifiers = new List<string> { subnetId.ToString(), "first_free" };
var result = RequestHelper.InvokeRequest(
"GET", ApiController.Subnets, null, null, null, identifiers.ToArray()
).GetAwaiter().GetResult();
if (result != null)
{
WriteObject(new { Ip = result.ToString() });
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetFirstFreeIPError", ErrorCategory.InvalidOperation, null));
}
}
private int GetSubnetId()
{
switch (ParameterSetName)
{
case "ByCIDR":
var subnet = RequestHelper.InvokeRequest(
"GET", ApiController.Subnets, ModelType.Subnetwork, null, null,
new[] { "cidr", CIDR! }
).GetAwaiter().GetResult() as Subnetwork;
if (subnet == null)
{
throw new ItemNotFoundException($"Subnet '{CIDR}' not found.");
}
return subnet.Id;
case "BySubnetObject":
return SubnetObject!.Id;
default:
return Id!.Value;
}
}
}

View File

@@ -0,0 +1,48 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves L2 domain information from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "L2Domain", DefaultParameterSetName = "ByID")]
[OutputType(typeof(Domain))]
public class GetL2DomainCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The L2 domain ID.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
protected override void ProcessRecord()
{
try
{
var identifiers = new List<string>();
if (Id.HasValue)
{
identifiers.Add(Id.Value.ToString());
}
var result = RequestHelper.InvokeRequest(
"GET", ApiController.L2Domains, ModelType.Domain, null, null,
identifiers.Count > 0 ? identifiers.ToArray() : null
).GetAwaiter().GetResult();
WriteResult(result);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetL2DomainError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,48 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves nameserver information from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "Nameserver", DefaultParameterSetName = "NoParams")]
[OutputType(typeof(Nameserver))]
public class GetNameserverCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The nameserver ID.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
protected override void ProcessRecord()
{
try
{
var identifiers = new List<string>();
if (Id.HasValue)
{
identifiers.Add(Id.Value.ToString());
}
var result = RequestHelper.InvokeRequest(
"GET", ApiController.Tools, ModelType.Nameserver, ApiSubController.Nameservers,
null, identifiers.Count > 0 ? identifiers.ToArray() : null
).GetAwaiter().GetResult();
WriteResult(result);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetNameserverError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,153 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Net;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves permission information from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "Permissions")]
public class GetPermissionsCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The address ID.")]
[ValidateNotNullOrEmpty]
public string? Id { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByIP",
HelpMessage = "The IP address to search for.")]
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 1,
ParameterSetName = "BySubnetId")]
[ValidateNotNullOrEmpty]
public IPAddress? IP { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByHostName",
HelpMessage = "The hostname to search for.")]
[ValidateNotNullOrEmpty]
public string? HostName { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByTag",
HelpMessage = "The tag ID.")]
[ValidateNotNullOrEmpty]
public string? TagId { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySubnetId",
HelpMessage = "The subnet ID.")]
[ValidateNotNullOrEmpty]
public string? SubnetId { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySubnetCIDR",
HelpMessage = "The subnet in CIDR notation.")]
[ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")]
[ValidateNotNullOrEmpty]
public string? SubnetCIDR { get; set; }
protected override void ProcessRecord()
{
try
{
var controller = ApiController.Addresses;
var identifiers = new List<string>();
switch (ParameterSetName)
{
case "ByID":
identifiers.Add(Id!);
break;
case "ByIP":
identifiers.Add("search");
identifiers.Add(IP!.ToString());
break;
case "ByHostName":
identifiers.Add("search_hostname");
identifiers.Add(HostName!);
break;
case "ByTag":
identifiers.Add("tags");
identifiers.Add(TagId!);
identifiers.Add("addresses");
break;
case "BySubnetId":
if (IP != null)
{
identifiers.Add(IP.ToString());
identifiers.Add(SubnetId!);
}
else
{
controller = ApiController.Subnets;
identifiers.Add(SubnetId!);
identifiers.Add("addresses");
}
break;
case "BySubnetCIDR":
controller = ApiController.Subnets;
var subnet = RequestHelper.InvokeRequest(
"GET", ApiController.Subnets, ModelType.Subnetwork, null, null,
new[] { "cidr", SubnetCIDR! }
).GetAwaiter().GetResult() as Subnetwork;
if (subnet == null)
{
throw new ItemNotFoundException($"Subnet '{SubnetCIDR}' not found.");
}
identifiers.Add(subnet.Id.ToString());
identifiers.Add("addresses");
break;
}
var result = RequestHelper.InvokeRequest(
"GET", controller, null, null, null, identifiers.ToArray()
).GetAwaiter().GetResult();
WriteResult(result);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetPermissionsError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,60 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves section information from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "Section", DefaultParameterSetName = "NoParams")]
[OutputType(typeof(Section))]
public class GetSectionCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The section ID.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
Position = 0,
ParameterSetName = "ByName",
HelpMessage = "The section name.")]
[ValidateNotNullOrEmpty]
public string? Name { get; set; }
protected override void ProcessRecord()
{
try
{
var identifiers = new List<string>();
if (ParameterSetName == "ByID" && Id.HasValue)
{
identifiers.Add(Id.Value.ToString());
}
else if (ParameterSetName == "ByName")
{
identifiers.Add(Name!);
}
var result = RequestHelper.InvokeRequest(
"GET", ApiController.Sections, ModelType.Section, null, null,
identifiers.Count > 0 ? identifiers.ToArray() : null
).GetAwaiter().GetResult();
WriteResult(result);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetSectionError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,277 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves subnet information from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "Subnet", DefaultParameterSetName = "NoParams")]
[OutputType(typeof(Subnetwork))]
public class GetSubnetCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByCIDR",
HelpMessage = "The subnet in CIDR notation (e.g., 192.168.1.0/24).")]
[ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")]
[ValidateNotNullOrEmpty]
public string? CIDR { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The subnet ID.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySectionId",
HelpMessage = "The section ID to get subnets from.")]
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 2,
ParameterSetName = "ByVlanNumber")]
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 1,
ParameterSetName = "ByVlanId")]
[ValidateNotNullOrEmpty]
public int? SectionId { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySectionName",
HelpMessage = "The section name to get subnets from.")]
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 3,
ParameterSetName = "ByVlanNumber")]
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 2,
ParameterSetName = "ByVlanId")]
[ValidateNotNullOrEmpty]
public string? SectionName { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByVrfId",
HelpMessage = "The VRF ID to get subnets from.")]
[ValidateNotNullOrEmpty]
public int? VrfId { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByVlanId",
HelpMessage = "The VLAN ID to get subnets from.")]
[ValidateNotNullOrEmpty]
public int? VlanId { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByVlanNumber",
HelpMessage = "The VLAN number to get subnets from.")]
[ValidateNotNullOrEmpty]
public int? VlanNumber { get; set; }
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 1,
ParameterSetName = "ByID",
HelpMessage = "Get child subnets (slaves).")]
public SwitchParameter Slaves { get; set; }
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 2,
ParameterSetName = "ByID",
HelpMessage = "Get child subnets recursively.")]
public SwitchParameter Recurse { get; set; }
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 1,
ParameterSetName = "ByVlanNumber",
HelpMessage = "The L2 domain ID to narrow VLAN search.")]
[ValidateNotNullOrEmpty]
public int? VlanDomainId { get; set; }
protected override void ProcessRecord()
{
try
{
var controller = ApiController.Subnets;
var identifiers = new List<string>();
switch (ParameterSetName)
{
case "ByCIDR":
identifiers.Add("cidr");
identifiers.Add(CIDR!);
break;
case "ByID":
identifiers.Add(Id!.Value.ToString());
if (Slaves.IsPresent)
{
identifiers.Add(Recurse.IsPresent ? "slaves_recursive" : "slaves");
}
break;
case "BySectionId":
controller = ApiController.Sections;
identifiers.Add(SectionId!.Value.ToString());
identifiers.Add("subnets");
break;
case "BySectionName":
controller = ApiController.Sections;
var section = GetSectionByName(SectionName!);
identifiers.Add(section.Id.ToString());
identifiers.Add("subnets");
break;
case "ByVrfId":
controller = ApiController.Vrf;
identifiers.Add(VrfId!.Value.ToString());
identifiers.Add("subnets");
break;
case "ByVlanId":
controller = ApiController.Vlan;
identifiers.Add(VlanId!.Value.ToString());
identifiers.Add("subnets");
AddSectionIdentifier(identifiers);
break;
case "ByVlanNumber":
controller = ApiController.Vlan;
var vlan = FindVlanByNumber(VlanNumber!.Value, VlanDomainId);
identifiers.Add(vlan.Id.ToString());
identifiers.Add("subnets");
AddSectionIdentifier(identifiers);
break;
}
var result = RequestHelper.InvokeRequest(
"GET", controller, ModelType.Subnetwork, null, null, identifiers.ToArray()
).GetAwaiter().GetResult();
WriteResult(result);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetSubnetError", ErrorCategory.InvalidOperation, null));
}
}
private Section GetSectionByName(string name)
{
var section = RequestHelper.InvokeRequest(
"GET", ApiController.Sections, ModelType.Section, null, null, new[] { name }
).GetAwaiter().GetResult() as Section;
if (section == null)
{
throw new ItemNotFoundException($"Section '{name}' not found.");
}
return section;
}
private Vlan FindVlanByNumber(int number, int? domainId)
{
var vlans = RequestHelper.InvokeRequest(
"GET", ApiController.Vlan, ModelType.Vlan, null, null,
new[] { "search", number.ToString() }
).GetAwaiter().GetResult();
if (vlans == null)
{
throw new ItemNotFoundException($"VLAN {number} not found.");
}
Vlan? foundVlan = null;
if (vlans is System.Collections.IEnumerable enumerable)
{
foreach (var v in enumerable)
{
if (v is Vlan vlan)
{
if (domainId.HasValue && vlan.DomainId != domainId.Value)
{
continue;
}
if (foundVlan != null)
{
throw new InvalidOperationException(
$"Multiple VLANs with number {number} exist. Specify VlanDomainId to narrow the search.");
}
foundVlan = vlan;
}
}
}
if (foundVlan == null)
{
throw new ItemNotFoundException($"VLAN {number} not found.");
}
return foundVlan;
}
private void AddSectionIdentifier(List<string> identifiers)
{
if (SectionId.HasValue)
{
identifiers.Add(SectionId.Value.ToString());
}
else if (!string.IsNullOrEmpty(SectionName))
{
var section = GetSectionByName(SectionName);
identifiers.Add(section.Id.ToString());
}
}
}

View File

@@ -0,0 +1,86 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves subnet usage statistics from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "SubnetUsage", DefaultParameterSetName = "ByID")]
public class GetSubnetUsageCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByCIDR",
HelpMessage = "The subnet in CIDR notation.")]
[ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")]
[ValidateNotNullOrEmpty]
public string? CIDR { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The subnet ID.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
Position = 0,
ParameterSetName = "BySubnetObject",
HelpMessage = "The subnet object.")]
[ValidateNotNullOrEmpty]
public Subnetwork? SubnetObject { get; set; }
protected override void ProcessRecord()
{
try
{
var subnetId = GetSubnetId();
var identifiers = new List<string> { subnetId.ToString(), "usage" };
var result = RequestHelper.InvokeRequest(
"GET", ApiController.Subnets, null, null, null, identifiers.ToArray()
).GetAwaiter().GetResult();
WriteResult(result);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetSubnetUsageError", ErrorCategory.InvalidOperation, null));
}
}
private int GetSubnetId()
{
switch (ParameterSetName)
{
case "ByCIDR":
var subnet = RequestHelper.InvokeRequest(
"GET", ApiController.Subnets, ModelType.Subnetwork, null, null,
new[] { "cidr", CIDR! }
).GetAwaiter().GetResult() as Subnetwork;
if (subnet == null)
{
throw new ItemNotFoundException($"Subnet '{CIDR}' not found.");
}
return subnet.Id;
case "BySubnetObject":
return SubnetObject!.Id;
default:
return Id!.Value;
}
}
}

View File

@@ -0,0 +1,94 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves tag information from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "Tag", DefaultParameterSetName = "NoParams")]
[OutputType(typeof(Tag))]
public class GetTagCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The tag ID.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByAddressObject",
HelpMessage = "Get the tag associated with this address.")]
[ValidateNotNullOrEmpty]
public Address? AddressObject { get; set; }
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySubnetObject",
HelpMessage = "Get the tag associated with this subnet.")]
[ValidateNotNullOrEmpty]
public Subnetwork? SubnetObject { get; set; }
protected override void ProcessRecord()
{
try
{
var identifiers = new List<string> { "tags" };
switch (ParameterSetName)
{
case "ByID":
if (Id.HasValue)
{
identifiers.Add(Id.Value.ToString());
}
break;
case "ByAddressObject":
if (AddressObject?.TagId > 0)
{
identifiers.Add(AddressObject.TagId.ToString());
}
else
{
return;
}
break;
case "BySubnetObject":
if (SubnetObject?.TagId > 0)
{
identifiers.Add(SubnetObject.TagId.ToString());
}
else
{
return;
}
break;
}
var result = RequestHelper.InvokeRequest(
"GET", ApiController.Addresses, ModelType.Tag, null, null, identifiers.ToArray()
).GetAwaiter().GetResult();
WriteResult(result);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetTagError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,121 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves VLAN information from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "Vlan", DefaultParameterSetName = "NoParams")]
[OutputType(typeof(Vlan))]
public class GetVlanCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The VLAN ID.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByNumber",
HelpMessage = "The VLAN number.")]
[ValidateNotNullOrEmpty]
public int? Number { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByL2Domain",
HelpMessage = "The L2 domain ID to get VLANs from.")]
[ValidateNotNullOrEmpty]
public int? L2DomainId { get; set; }
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySubnetObject",
HelpMessage = "Get the VLAN associated with this subnet.")]
[ValidateNotNullOrEmpty]
public Subnetwork? SubnetObject { get; set; }
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByDomainObject",
HelpMessage = "Get VLANs in this L2 domain.")]
[ValidateNotNullOrEmpty]
public Domain? DomainObject { get; set; }
protected override void ProcessRecord()
{
try
{
var controller = ApiController.Vlan;
var identifiers = new List<string>();
switch (ParameterSetName)
{
case "ByID":
identifiers.Add(Id!.Value.ToString());
break;
case "ByNumber":
identifiers.Add("search");
identifiers.Add(Number!.Value.ToString());
break;
case "ByL2Domain":
controller = ApiController.L2Domains;
identifiers.Add(L2DomainId!.Value.ToString());
identifiers.Add(ApiSubController.Vlans.ToString().ToLowerInvariant());
break;
case "BySubnetObject":
if (SubnetObject == null || SubnetObject.VlanId <= 0)
{
return;
}
identifiers.Add(SubnetObject.VlanId.ToString());
break;
case "ByDomainObject":
if (DomainObject != null)
{
var result = RequestHelper.InvokeRequest(
"GET", ApiController.L2Domains, ModelType.Vlan, ApiSubController.Vlans,
null, new[] { DomainObject.Id.ToString() }
).GetAwaiter().GetResult();
WriteResult(result);
}
return;
}
var vlanResult = RequestHelper.InvokeRequest(
"GET", controller, ModelType.Vlan, null, null,
identifiers.Count > 0 ? identifiers.ToArray() : null
).GetAwaiter().GetResult();
WriteResult(vlanResult);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetVlanError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,48 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Retrieves VRF information from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Get, "Vrf", DefaultParameterSetName = "NoParams")]
[OutputType(typeof(Vrf))]
public class GetVrfCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByID",
HelpMessage = "The VRF ID.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
protected override void ProcessRecord()
{
try
{
var identifiers = new List<string>();
if (Id.HasValue)
{
identifiers.Add(Id.Value.ToString());
}
var result = RequestHelper.InvokeRequest(
"GET", ApiController.Vrf, ModelType.Vrf, null, null,
identifiers.Count > 0 ? identifiers.ToArray() : null
).GetAwaiter().GetResult();
WriteResult(result);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "GetVrfError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,142 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Creates a new IP address entry in phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.New, "Address", DefaultParameterSetName = "BySubnetId")]
[OutputType(typeof(Address))]
public class NewAddressCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySubnetId",
HelpMessage = "The subnet ID to create the address in.")]
[ValidateNotNullOrEmpty]
public int? SubnetId { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "BySubnetObject",
HelpMessage = "The subnet object to create the address in.")]
[ValidateNotNullOrEmpty]
public Subnetwork? SubnetObject { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 1,
HelpMessage = "The IP address to create.")]
[ValidateNotNullOrEmpty]
public string IP { get; set; } = string.Empty;
[Parameter(Mandatory = false, Position = 2, HelpMessage = "Mark this address as a gateway.")]
public SwitchParameter Gateway { get; set; }
[Parameter(Mandatory = false, Position = 3, HelpMessage = "Description for the address.")]
public string? Description { get; set; }
[Parameter(Mandatory = false, Position = 4, HelpMessage = "Hostname for the address.")]
public string? Hostname { get; set; }
[Parameter(Mandatory = false, Position = 5, HelpMessage = "MAC address.")]
public string? MAC { get; set; }
[Parameter(Mandatory = false, Position = 6, HelpMessage = "Owner of the address.")]
public string? Owner { get; set; }
[Parameter(Mandatory = false, Position = 7, HelpMessage = "Tag ID for the address.")]
public int? TagId { get; set; }
[Parameter(Mandatory = false, Position = 8, HelpMessage = "Ignore PTR record generation.")]
public SwitchParameter PTRIgnore { get; set; }
[Parameter(Mandatory = false, Position = 9, HelpMessage = "PTR record ID.")]
public int? PTRId { get; set; }
[Parameter(Mandatory = false, Position = 10, HelpMessage = "Note for the address.")]
public string? Note { get; set; }
[Parameter(Mandatory = false, Position = 11, HelpMessage = "Exclude from ping scanning.")]
public SwitchParameter ExcludePing { get; set; }
[Parameter(Mandatory = false, Position = 12, HelpMessage = "Associated device ID.")]
public int? DeviceId { get; set; }
[Parameter(Mandatory = false, Position = 13, HelpMessage = "Port information.")]
public string? Port { get; set; }
[Parameter(Mandatory = false, Position = 14, HelpMessage = "Custom fields as a hashtable or PSObject.")]
public object? CustomFields { get; set; }
protected override void ProcessRecord()
{
try
{
var actualSubnetId = ParameterSetName == "BySubnetObject"
? SubnetObject!.Id
: SubnetId!.Value;
var body = BuildRequestBody(actualSubnetId);
var result = RequestHelper.InvokeRequest(
"POST", ApiController.Addresses, null, null, body
).GetAwaiter().GetResult();
if (result != null)
{
// Fetch the created address to return it
var address = RequestHelper.InvokeRequest(
"GET", ApiController.Addresses, ModelType.Address, null, null,
new[] { "search", IP }
).GetAwaiter().GetResult();
WriteResult(address);
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "NewAddressError", ErrorCategory.InvalidOperation, null));
}
}
private Dictionary<string, object> BuildRequestBody(int subnetId)
{
var body = new Dictionary<string, object>
{
["subnetId"] = subnetId,
["ip"] = IP
};
if (Gateway.IsPresent) body["is_gateway"] = "1";
if (!string.IsNullOrEmpty(Description)) body["description"] = Description;
if (!string.IsNullOrEmpty(Hostname)) body["hostname"] = Hostname;
if (!string.IsNullOrEmpty(MAC)) body["mac"] = MAC;
if (!string.IsNullOrEmpty(Owner)) body["owner"] = Owner;
if (TagId.HasValue) body["tag"] = TagId.Value;
if (PTRIgnore.IsPresent) body["PTRignore"] = "1";
if (PTRId.HasValue) body["PTR"] = PTRId.Value;
if (!string.IsNullOrEmpty(Note)) body["note"] = Note;
if (ExcludePing.IsPresent) body["excludePing"] = "1";
if (DeviceId.HasValue) body["deviceId"] = DeviceId.Value;
if (!string.IsNullOrEmpty(Port)) body["port"] = Port;
foreach (var kvp in ConvertCustomFields(CustomFields))
{
body[kvp.Key] = kvp.Value;
}
return body;
}
}

View File

@@ -0,0 +1,118 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Creates a new address using the first available IP in a subnet.
/// </summary>
[Cmdlet(VerbsCommon.New, "FirstFreeIP")]
[OutputType(typeof(Address))]
public class NewFirstFreeIPCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
HelpMessage = "The subnet ID to create the address in.")]
[ValidateNotNullOrEmpty]
public string SubnetId { get; set; } = string.Empty;
[Parameter(Mandatory = false, Position = 1, HelpMessage = "Mark this address as a gateway.")]
public SwitchParameter Gateway { get; set; }
[Parameter(Mandatory = false, Position = 2, HelpMessage = "Description for the address.")]
public string? Description { get; set; }
[Parameter(Mandatory = false, Position = 3, HelpMessage = "Hostname for the address.")]
public string? Hostname { get; set; }
[Parameter(Mandatory = false, Position = 4, HelpMessage = "MAC address.")]
public string? MAC { get; set; }
[Parameter(Mandatory = false, Position = 5, HelpMessage = "Owner of the address.")]
public string? Owner { get; set; }
[Parameter(Mandatory = false, Position = 6, HelpMessage = "Tag ID for the address.")]
public string? TagId { get; set; }
[Parameter(Mandatory = false, Position = 7, HelpMessage = "Ignore PTR record generation.")]
public SwitchParameter PTRIgnore { get; set; }
[Parameter(Mandatory = false, Position = 8, HelpMessage = "PTR record ID.")]
public string? PTRId { get; set; }
[Parameter(Mandatory = false, Position = 9, HelpMessage = "Note for the address.")]
public string? Note { get; set; }
[Parameter(Mandatory = false, Position = 10, HelpMessage = "Exclude from ping scanning.")]
public SwitchParameter ExcludePing { get; set; }
[Parameter(Mandatory = false, Position = 11, HelpMessage = "Associated device ID.")]
public string? DeviceId { get; set; }
[Parameter(Mandatory = false, Position = 12, HelpMessage = "Port information.")]
public string? Port { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Custom fields as a hashtable or PSObject.")]
public object? CustomFields { get; set; }
protected override void ProcessRecord()
{
try
{
var identifiers = new[] { "first_free" };
var body = BuildRequestBody();
var result = RequestHelper.InvokeRequest(
"POST", ApiController.Addresses, null, null, body, identifiers
).GetAwaiter().GetResult();
if (result != null)
{
var ip = result.ToString();
var address = RequestHelper.InvokeRequest(
"GET", ApiController.Addresses, ModelType.Address, null, null,
new[] { "search", ip! }
).GetAwaiter().GetResult();
WriteResult(address);
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "NewFirstFreeIPError", ErrorCategory.InvalidOperation, null));
}
}
private Dictionary<string, object> BuildRequestBody()
{
var body = new Dictionary<string, object>
{
["subnetId"] = SubnetId
};
if (Gateway.IsPresent) body["is_gateway"] = "1";
if (!string.IsNullOrEmpty(Description)) body["description"] = Description;
if (!string.IsNullOrEmpty(Hostname)) body["hostname"] = Hostname;
if (!string.IsNullOrEmpty(MAC)) body["mac"] = MAC;
if (!string.IsNullOrEmpty(Owner)) body["owner"] = Owner;
if (!string.IsNullOrEmpty(TagId)) body["tag"] = TagId;
if (PTRIgnore.IsPresent) body["PTRignore"] = "1";
if (!string.IsNullOrEmpty(PTRId)) body["PTR"] = PTRId;
if (!string.IsNullOrEmpty(Note)) body["note"] = Note;
if (ExcludePing.IsPresent) body["excludePing"] = "1";
if (!string.IsNullOrEmpty(DeviceId)) body["deviceId"] = DeviceId;
if (!string.IsNullOrEmpty(Port)) body["port"] = Port;
foreach (var kvp in ConvertCustomFields(CustomFields))
{
body[kvp.Key] = kvp.Value;
}
return body;
}
}

View File

@@ -0,0 +1,92 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Creates a new phpIPAM API session.
/// </summary>
[Cmdlet(VerbsCommon.New, "Session", DefaultParameterSetName = "Credentials")]
[OutputType(typeof(Session))]
public class NewSessionCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
HelpMessage = "The phpIPAM server URL (must start with http:// or https://).")]
[ValidateNotNullOrEmpty]
[ValidatePattern("^https?://")]
public string URL { get; set; } = string.Empty;
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 1,
HelpMessage = "The API application ID configured in phpIPAM.")]
[ValidateNotNullOrEmpty]
public string AppID { get; set; } = string.Empty;
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 2,
ParameterSetName = "Credentials",
HelpMessage = "The credentials (username and password) for authentication.")]
[ValidateNotNullOrEmpty]
public PSCredential? Credentials { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 2,
ParameterSetName = "Token",
HelpMessage = "The static API token for authentication.")]
[ValidateNotNullOrEmpty]
public string? Token { get; set; }
[Parameter(
Mandatory = false,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 3,
HelpMessage = "If specified, SSL certificate errors will be ignored.")]
public SwitchParameter IgnoreSSL { get; set; }
protected override void ProcessRecord()
{
try
{
Session session;
if (ParameterSetName == "Credentials" && Credentials != null)
{
session = SessionManager.CreateSessionWithCredentialsAsync(
URL,
AppID,
Credentials,
IgnoreSSL.IsPresent
).GetAwaiter().GetResult();
}
else if (ParameterSetName == "Token" && Token != null)
{
session = SessionManager.CreateSessionWithToken(URL, AppID, Token);
}
else
{
throw new ArgumentException("Invalid parameter set. Provide either Credentials or Token.");
}
WriteObject(session);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "NewSessionError", ErrorCategory.AuthenticationError, null));
}
}
}

View File

@@ -0,0 +1,143 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Creates a new subnet in phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.New, "Subnet")]
[OutputType(typeof(Subnetwork))]
public class NewSubnetCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
HelpMessage = "The subnet in CIDR notation (e.g., 192.168.1.0/24).")]
[ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")]
[ValidateNotNullOrEmpty]
public string CIDR { get; set; } = string.Empty;
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 1,
HelpMessage = "The section ID to create the subnet in.")]
[ValidateNotNullOrEmpty]
public int SectionId { get; set; }
[Parameter(Mandatory = false, Position = 2, HelpMessage = "Description for the subnet.")]
public string? Description { get; set; }
[Parameter(Mandatory = false, Position = 3, HelpMessage = "VLAN ID to associate.")]
public int? VlanId { get; set; }
[Parameter(Mandatory = false, Position = 4, HelpMessage = "VRF ID to associate.")]
public int? VrfId { get; set; }
[Parameter(Mandatory = false, Position = 5, HelpMessage = "Master subnet ID for hierarchy.")]
public int? MasterSubnetId { get; set; }
[Parameter(Mandatory = false, Position = 6, HelpMessage = "Nameserver ID.")]
public int? NameserverId { get; set; }
[Parameter(Mandatory = false, Position = 7, HelpMessage = "Show subnet name.")]
public SwitchParameter ShowName { get; set; }
[Parameter(Mandatory = false, Position = 8, HelpMessage = "Enable recursive DNS.")]
public SwitchParameter DNSRecursive { get; set; }
[Parameter(Mandatory = false, Position = 9, HelpMessage = "Enable DNS records.")]
public SwitchParameter DNSRecords { get; set; }
[Parameter(Mandatory = false, Position = 10, HelpMessage = "Allow IP requests.")]
public SwitchParameter AllowRequests { get; set; }
[Parameter(Mandatory = false, Position = 11, HelpMessage = "Scan agent ID.")]
public int? ScanAgentId { get; set; }
[Parameter(Mandatory = false, Position = 12, HelpMessage = "Enable subnet discovery.")]
public SwitchParameter DiscoverSubnet { get; set; }
[Parameter(Mandatory = false, Position = 13, HelpMessage = "Mark subnet as full.")]
public SwitchParameter IsFull { get; set; }
[Parameter(Mandatory = false, Position = 14, HelpMessage = "Tag ID for the subnet.")]
public int? TagId { get; set; }
[Parameter(Mandatory = false, Position = 15, HelpMessage = "Usage threshold percentage (1-100).")]
[ValidateRange(1, 100)]
public int? Threshold { get; set; }
[Parameter(Mandatory = false, Position = 16, HelpMessage = "Location ID.")]
public int? LocationId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Custom fields as a hashtable or PSObject.")]
public object? CustomFields { get; set; }
protected override void ProcessRecord()
{
try
{
var body = BuildRequestBody();
var result = RequestHelper.InvokeRequest(
"POST", ApiController.Subnets, null, null, body
).GetAwaiter().GetResult();
if (result != null)
{
// Get the created subnet
var subnet = RequestHelper.InvokeRequest(
"GET", ApiController.Subnets, ModelType.Subnetwork, null, null,
new[] { "cidr", CIDR }
).GetAwaiter().GetResult();
WriteResult(subnet);
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "NewSubnetError", ErrorCategory.InvalidOperation, null));
}
}
private Dictionary<string, object> BuildRequestBody()
{
var parts = CIDR.Split('/');
var body = new Dictionary<string, object>
{
["subnet"] = parts[0],
["mask"] = parts[1],
["sectionId"] = SectionId
};
if (!string.IsNullOrEmpty(Description)) body["description"] = Description;
if (VlanId.HasValue) body["vlanId"] = VlanId.Value;
if (VrfId.HasValue) body["vrfId"] = VrfId.Value;
if (MasterSubnetId.HasValue) body["masterSubnetId"] = MasterSubnetId.Value;
if (NameserverId.HasValue) body["nameserverId"] = NameserverId.Value;
if (ShowName.IsPresent) body["showName"] = "1";
if (DNSRecursive.IsPresent) body["DNSrecursive"] = "1";
if (DNSRecords.IsPresent) body["DNSrecords"] = "1";
if (AllowRequests.IsPresent) body["allowRequests"] = "1";
if (ScanAgentId.HasValue) body["scanAgent"] = ScanAgentId.Value;
if (DiscoverSubnet.IsPresent) body["discoverSubnet"] = "1";
if (IsFull.IsPresent) body["isFull"] = "1";
if (TagId.HasValue) body["state"] = TagId.Value;
if (Threshold.HasValue) body["threshold"] = Threshold.Value;
if (LocationId.HasValue) body["location"] = LocationId.Value;
foreach (var kvp in ConvertCustomFields(CustomFields))
{
body[kvp.Key] = kvp.Value;
}
return body;
}
}

View File

@@ -0,0 +1,53 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Removes an IP address entry from phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Remove, "Address", DefaultParameterSetName = "ById", SupportsShouldProcess = true)]
public class RemoveAddressCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ById",
HelpMessage = "The address ID to remove.")]
[ValidateNotNullOrEmpty]
public int Id { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
Position = 0,
ParameterSetName = "ByAddressObject",
HelpMessage = "The address object to remove.")]
[ValidateNotNullOrEmpty]
public Address? AddressObject { get; set; }
protected override void ProcessRecord()
{
try
{
var addressId = ParameterSetName == "ById" ? Id : AddressObject!.Id;
var identifiers = new[] { addressId.ToString() };
if (ShouldProcess($"Address ID: {addressId}", "Remove"))
{
RequestHelper.InvokeRequest(
"DELETE", ApiController.Addresses, null, null, null, identifiers
).GetAwaiter().GetResult();
WriteVerbose($"Address {addressId} removed successfully.");
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "RemoveAddressError", ErrorCategory.InvalidOperation, null));
}
}
}

View File

@@ -0,0 +1,128 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Updates an existing IP address entry in phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Set, "Address", DefaultParameterSetName = "ById")]
[OutputType(typeof(Address))]
public class SetAddressCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ById",
HelpMessage = "The address ID to update.")]
[ValidateNotNullOrEmpty]
public int? Id { get; set; }
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
ParameterSetName = "ByAddressObject",
HelpMessage = "The address object to update.")]
[ValidateNotNullOrEmpty]
public Address? AddressObject { get; set; }
[Parameter(Position = 1, HelpMessage = "Set as gateway address.")]
public bool? Gateway { get; set; }
[Parameter(Position = 2, HelpMessage = "Description for the address.")]
public string? Description { get; set; }
[Parameter(Position = 3, HelpMessage = "Hostname for the address.")]
public string? Hostname { get; set; }
[Parameter(Position = 4, HelpMessage = "MAC address.")]
public string? MAC { get; set; }
[Parameter(Position = 5, HelpMessage = "Owner of the address.")]
public string? Owner { get; set; }
[Parameter(Position = 6, HelpMessage = "Tag ID for the address.")]
public int? TagId { get; set; }
[Parameter(Position = 7, HelpMessage = "Ignore PTR record generation.")]
public bool? PTRIgnore { get; set; }
[Parameter(Position = 8, HelpMessage = "PTR record ID.")]
public int? PTRId { get; set; }
[Parameter(Position = 9, HelpMessage = "Note for the address.")]
public string? Note { get; set; }
[Parameter(Position = 10, HelpMessage = "Exclude from ping scanning.")]
public bool? ExcludePing { get; set; }
[Parameter(Position = 11, HelpMessage = "Associated device ID.")]
public int? DeviceId { get; set; }
[Parameter(Position = 12, HelpMessage = "Port information.")]
public string? Port { get; set; }
[Parameter(HelpMessage = "Custom fields as a hashtable or PSObject.")]
public object? CustomFields { get; set; }
protected override void ProcessRecord()
{
try
{
var addressId = ParameterSetName == "ById" ? Id!.Value : AddressObject!.Id;
var identifiers = new[] { addressId.ToString() };
var body = BuildRequestBody();
try
{
RequestHelper.InvokeRequest(
"PATCH", ApiController.Addresses, null, null, body, identifiers
).GetAwaiter().GetResult();
}
finally
{
// Always return the updated address
var address = RequestHelper.InvokeRequest(
"GET", ApiController.Addresses, ModelType.Address, null, null, identifiers
).GetAwaiter().GetResult();
WriteResult(address);
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "SetAddressError", ErrorCategory.InvalidOperation, null));
}
}
private Dictionary<string, object> BuildRequestBody()
{
var body = new Dictionary<string, object>();
if (Gateway.HasValue) body["is_gateway"] = Gateway.Value;
if (!string.IsNullOrEmpty(Description)) body["description"] = Description;
if (!string.IsNullOrEmpty(Hostname)) body["hostname"] = Hostname;
if (!string.IsNullOrEmpty(MAC)) body["mac"] = MAC;
if (!string.IsNullOrEmpty(Owner)) body["owner"] = Owner;
if (TagId.HasValue) body["tag"] = TagId.Value;
if (PTRIgnore.HasValue) body["PTRignore"] = PTRIgnore.Value;
if (PTRId.HasValue) body["PTR"] = PTRId.Value;
if (!string.IsNullOrEmpty(Note)) body["note"] = Note;
if (ExcludePing.HasValue) body["excludePing"] = ExcludePing.Value;
if (DeviceId.HasValue) body["deviceId"] = DeviceId.Value;
if (!string.IsNullOrEmpty(Port)) body["port"] = Port;
foreach (var kvp in ConvertCustomFields(CustomFields))
{
body[kvp.Key] = kvp.Value;
}
return body;
}
}

View File

@@ -0,0 +1,123 @@
namespace PS.IPAM.Cmdlets;
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PS.IPAM.Helpers;
/// <summary>
/// Updates an existing subnet in phpIPAM.
/// </summary>
[Cmdlet(VerbsCommon.Set, "Subnet")]
[OutputType(typeof(Subnetwork))]
public class SetSubnetCmdlet : BaseCmdlet
{
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
HelpMessage = "The subnet ID to update.")]
[ValidateNotNullOrEmpty]
public int Id { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Description for the subnet.")]
public string? Description { get; set; }
[Parameter(Mandatory = false, HelpMessage = "VLAN ID to associate.")]
public int? VlanId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "VRF ID to associate.")]
public int? VrfId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Master subnet ID for hierarchy.")]
public int? MasterSubnetId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Nameserver ID.")]
public int? NameserverId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Show subnet name.")]
public SwitchParameter ShowName { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Enable recursive DNS.")]
public SwitchParameter DNSRecursive { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Enable DNS records.")]
public SwitchParameter DNSRecords { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Allow IP requests.")]
public SwitchParameter AllowRequests { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Scan agent ID.")]
public int? ScanAgentId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Enable subnet discovery.")]
public SwitchParameter DiscoverSubnet { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Mark subnet as full.")]
public SwitchParameter IsFull { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Tag ID for the subnet.")]
public int? TagId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Usage threshold percentage (1-100).")]
[ValidateRange(1, 100)]
public int? Threshold { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Location ID.")]
public int? LocationId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Custom fields as a hashtable or PSObject.")]
public object? CustomFields { get; set; }
protected override void ProcessRecord()
{
try
{
var identifiers = new[] { Id.ToString() };
var body = BuildRequestBody();
RequestHelper.InvokeRequest(
"PATCH", ApiController.Subnets, null, null, body, identifiers
).GetAwaiter().GetResult();
var subnet = RequestHelper.InvokeRequest(
"GET", ApiController.Subnets, ModelType.Subnetwork, null, null, identifiers
).GetAwaiter().GetResult();
WriteResult(subnet);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "SetSubnetError", ErrorCategory.InvalidOperation, null));
}
}
private Dictionary<string, object> BuildRequestBody()
{
var body = new Dictionary<string, object>();
if (!string.IsNullOrEmpty(Description)) body["description"] = Description;
if (VlanId.HasValue) body["vlanId"] = VlanId.Value;
if (VrfId.HasValue) body["vrfId"] = VrfId.Value;
if (MasterSubnetId.HasValue) body["masterSubnetId"] = MasterSubnetId.Value;
if (NameserverId.HasValue) body["nameserverId"] = NameserverId.Value;
if (ShowName.IsPresent) body["showName"] = "1";
if (DNSRecursive.IsPresent) body["DNSrecursive"] = "1";
if (DNSRecords.IsPresent) body["DNSrecords"] = "1";
if (AllowRequests.IsPresent) body["allowRequests"] = "1";
if (ScanAgentId.HasValue) body["scanAgent"] = ScanAgentId.Value;
if (DiscoverSubnet.IsPresent) body["discoverSubnet"] = "1";
if (IsFull.IsPresent) body["isFull"] = "1";
if (TagId.HasValue) body["state"] = TagId.Value;
if (Threshold.HasValue) body["threshold"] = Threshold.Value;
if (LocationId.HasValue) body["location"] = LocationId.Value;
foreach (var kvp in ConvertCustomFields(CustomFields))
{
body[kvp.Key] = kvp.Value;
}
return body;
}
}

View File

@@ -0,0 +1,441 @@
namespace PS.IPAM.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
/// <summary>
/// Helper class for making HTTP requests to the phpIPAM API.
/// </summary>
public static class RequestHelper
{
/// <summary>
/// Handler for testing - allows injecting a mock HTTP handler.
/// </summary>
public static HttpMessageHandler? TestHttpHandler { get; set; }
/// <summary>
/// Invokes an HTTP request to the phpIPAM API.
/// </summary>
/// <param name="method">The HTTP method (GET, POST, PATCH, DELETE).</param>
/// <param name="controller">The API controller to call.</param>
/// <param name="modelType">The expected model type for response conversion.</param>
/// <param name="subController">Optional sub-controller for nested endpoints.</param>
/// <param name="parameters">Optional request body parameters.</param>
/// <param name="identifiers">Optional path identifiers.</param>
/// <param name="ignoreSsl">Whether to ignore SSL certificate errors.</param>
/// <returns>The deserialized response data, or null if not found.</returns>
public static async Task<object?> InvokeRequestAsync(
HttpMethod method,
ApiController controller,
ModelType? modelType = null,
ApiSubController? subController = null,
object? parameters = null,
string[]? identifiers = null,
bool ignoreSsl = false)
{
EnsureValidSession();
var session = SessionManager.CurrentSession!;
var uri = BuildUri(session, controller, subController, identifiers);
using var client = SessionManager.CreateHttpClient(ignoreSsl, TestHttpHandler);
ConfigureClient(client, session);
var response = await SendRequestAsync(client, method, uri, parameters);
if (response == null)
{
return null;
}
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
throw new HttpRequestException($"Request failed with status {response.StatusCode}");
}
if (string.IsNullOrEmpty(responseContent))
{
return null;
}
return ParseResponse(responseContent, modelType);
}
/// <summary>
/// Overload for backward compatibility using string method names.
/// </summary>
public static Task<object?> InvokeRequest(
string method,
ApiController controller,
ModelType? modelType = null,
ApiSubController? subController = null,
object? parameters = null,
string[]? identifiers = null,
bool ignoreSsl = false)
{
var httpMethod = method.ToUpperInvariant() switch
{
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PATCH" => new HttpMethod("PATCH"),
"DELETE" => HttpMethod.Delete,
"PUT" => HttpMethod.Put,
_ => throw new ArgumentException($"Unsupported HTTP method: {method}", nameof(method))
};
return InvokeRequestAsync(httpMethod, controller, modelType, subController, parameters, identifiers, ignoreSsl);
}
/// <summary>
/// Refreshes an expired session.
/// </summary>
public static async Task RefreshSessionAsync()
{
var session = SessionManager.CurrentSession;
if (session == null)
{
throw new InvalidOperationException("No session available!");
}
var status = SessionManager.GetSessionStatus();
if (status == SessionStatus.Valid)
{
// Just refresh the token
await InvokeRequestAsync(new HttpMethod("PATCH"), ApiController.User);
return;
}
if (status == SessionStatus.Expired && session.Credentials is PSCredential creds)
{
await SessionManager.CreateSessionWithCredentialsAsync(
session.URL,
session.AppID,
creds,
false
);
}
}
private static void EnsureValidSession()
{
var status = SessionManager.GetSessionStatus();
switch (status)
{
case SessionStatus.NoSession:
throw new InvalidOperationException("No session available!");
case SessionStatus.Expired:
RefreshSessionAsync().GetAwaiter().GetResult();
break;
}
}
private static string BuildUri(
Session session,
ApiController controller,
ApiSubController? subController,
string[]? identifiers)
{
var controllerName = GetControllerName(controller);
var uri = $"{session.URL}/api/{session.AppID}/{controllerName}";
if (subController.HasValue)
{
uri += $"/{GetSubControllerName(subController.Value)}";
}
if (identifiers is { Length: > 0 })
{
uri += $"/{string.Join("/", identifiers)}/";
}
return uri;
}
private static string GetControllerName(ApiController controller) => controller switch
{
ApiController.User => "user",
ApiController.Vlan => "vlan",
ApiController.Subnets => "subnets",
ApiController.Addresses => "addresses",
ApiController.Sections => "sections",
ApiController.Vrf => "vrf",
ApiController.L2Domains => "l2domains",
ApiController.Tools => "tools",
_ => throw new ArgumentOutOfRangeException(nameof(controller))
};
private static string GetSubControllerName(ApiSubController subController) => subController switch
{
ApiSubController.Nameservers => "nameservers",
ApiSubController.Tags => "tags",
ApiSubController.Devices => "devices",
ApiSubController.DeviceTypes => "device_types",
ApiSubController.Vlans => "vlans",
ApiSubController.Vrfs => "vrfs",
ApiSubController.ScanAgents => "scanagents",
ApiSubController.Locations => "locations",
ApiSubController.Nat => "nat",
ApiSubController.Racks => "racks",
_ => throw new ArgumentOutOfRangeException(nameof(subController))
};
private static void ConfigureClient(HttpClient client, Session session)
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
switch (session.AuthType)
{
case AuthType.Credentials:
client.DefaultRequestHeaders.Add("token", session.Token);
break;
case AuthType.Token:
client.DefaultRequestHeaders.Add("phpipam-token", session.Token);
break;
}
}
private static async Task<HttpResponseMessage?> SendRequestAsync(
HttpClient client,
HttpMethod method,
string uri,
object? parameters)
{
try
{
if (method == HttpMethod.Get)
{
return await client.GetAsync(uri);
}
if (method == HttpMethod.Delete)
{
return await client.DeleteAsync(uri);
}
var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}";
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
if (method == HttpMethod.Post)
{
return await client.PostAsync(uri, content);
}
// PATCH, PUT, etc.
var request = new HttpRequestMessage(method, uri) { Content = content };
return await client.SendAsync(request);
}
catch (HttpRequestException ex) when (ex.Message.Contains("404"))
{
return null;
}
}
private static object? ParseResponse(string responseContent, ModelType? modelType)
{
var jsonResponse = JsonConvert.DeserializeObject<dynamic>(responseContent);
if (jsonResponse == null)
{
return null;
}
if (!modelType.HasValue)
{
return jsonResponse.data;
}
return ConvertToTypedObjects(jsonResponse, modelType.Value);
}
private static object? ConvertToTypedObjects(dynamic jsonResponse, ModelType modelType)
{
if (jsonResponse?.data == null)
{
return null;
}
var data = jsonResponse.data;
if (data is JArray array)
{
return array.Select(item => ConvertSingleObject(item, modelType)).ToList();
}
return ConvertSingleObject(data, modelType);
}
private static object? ConvertSingleObject(dynamic item, ModelType modelType)
{
if (item is not JObject jobject)
{
return null;
}
var customFields = ExtractCustomFields(jobject);
return modelType switch
{
ModelType.Address => CreateAddress(jobject, customFields),
ModelType.Vlan => CreateVlan(jobject, customFields),
ModelType.Subnetwork => CreateSubnetwork(jobject, customFields),
ModelType.Vrf => CreateVrf(jobject, customFields),
ModelType.Section => CreateSection(jobject),
ModelType.Tag => CreateTag(jobject),
ModelType.Nameserver => CreateNameserver(jobject),
ModelType.Domain => CreateDomain(jobject),
_ => jobject.ToObject<object>()
};
}
private static Dictionary<string, object>? ExtractCustomFields(JObject jobject)
{
var customFields = new Dictionary<string, object>();
foreach (var prop in jobject.Properties().Where(p => p.Name.StartsWith("custom_")))
{
customFields[prop.Name] = prop.Value?.ToObject<object>() ?? string.Empty;
}
return customFields.Count > 0 ? customFields : null;
}
#region Model Factory Methods
private static Address CreateAddress(JObject obj, Dictionary<string, object>? customFields) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["subnetId"]?.ToObject<int>() ?? 0,
obj["ip"]?.ToString() ?? "",
obj["is_gateway"]?.ToObject<bool>() ?? false,
obj["description"]?.ToString() ?? "",
obj["hostname"]?.ToString() ?? "",
obj["mac"]?.ToString() ?? "",
obj["owner"]?.ToString() ?? "",
obj["tag"]?.ToObject<int>() ?? 0,
obj["deviceId"]?.ToObject<int>() ?? 0,
obj["location"]?.ToString() ?? "",
obj["port"]?.ToString() ?? "",
obj["note"]?.ToString() ?? "",
obj["lastSeen"]?.ToObject<DateTime?>(),
obj["excludePing"]?.ToObject<bool>() ?? false,
obj["PTRignore"]?.ToObject<bool>() ?? false,
obj["PTR"]?.ToObject<int>() ?? 0,
obj["firewallAddressObject"]?.ToString() ?? "",
obj["editDate"]?.ToObject<DateTime?>(),
obj["customer_id"]?.ToObject<int>() ?? 0,
customFields
);
private static Vlan CreateVlan(JObject obj, Dictionary<string, object>? customFields) => new(
obj["vlanId"]?.ToObject<int>() ?? 0,
obj["domainId"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["number"]?.ToObject<int>() ?? 0,
obj["description"]?.ToString() ?? "",
obj["editDate"]?.ToObject<DateTime?>(),
obj["customer_id"]?.ToObject<int>() ?? 0,
customFields
);
private static Subnetwork CreateSubnetwork(JObject obj, Dictionary<string, object>? customFields) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["subnet"]?.ToString() ?? "",
obj["mask"]?.ToObject<int>() ?? 0,
obj["sectionId"]?.ToObject<int>() ?? 0,
obj["description"]?.ToString() ?? "",
obj["linked_subnet"]?.ToString() ?? "",
obj["firewallAddressObject"]?.ToString() ?? "",
obj["vrfId"]?.ToObject<int>() ?? 0,
obj["masterSubnetId"]?.ToObject<int>() ?? 0,
obj["allowRequests"]?.ToObject<bool>() ?? false,
obj["vlanId"]?.ToObject<int>() ?? 0,
obj["showName"]?.ToObject<bool>() ?? false,
obj["deviceId"]?.ToObject<int>() ?? 0,
obj["permissions"]?.ToString() ?? "",
obj["pingSubnet"]?.ToObject<bool>() ?? false,
obj["discoverSubnet"]?.ToObject<bool>() ?? false,
obj["resolveDNS"]?.ToObject<bool>() ?? false,
obj["DNSrecursive"]?.ToObject<bool>() ?? false,
obj["DNSrecords"]?.ToObject<bool>() ?? false,
obj["nameserverId"]?.ToObject<int>() ?? 0,
obj["scanAgent"]?.ToObject<bool>() ?? false,
obj["isFolder"]?.ToObject<bool>() ?? false,
obj["isFull"]?.ToObject<bool>() ?? false,
obj["isPool"]?.ToObject<bool>() ?? false,
obj["state"]?.ToObject<int>() ?? 0,
obj["threshold"]?.ToObject<int>() ?? 0,
obj["location"]?.ToObject<int>() ?? 0,
obj["editDate"]?.ToObject<DateTime?>(),
obj["lastScan"]?.ToObject<DateTime?>(),
obj["lastDiscovery"]?.ToObject<DateTime?>(),
obj["calculation"]?.ToObject<object>() ?? new object(),
customFields
);
private static Vrf CreateVrf(JObject obj, Dictionary<string, object>? customFields) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["rd"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["sections"]?.ToString() ?? "",
obj["editDate"]?.ToObject<DateTime?>(),
customFields
);
private static Section CreateSection(JObject obj) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["masterSection"]?.ToObject<int>() ?? 0,
obj["permissions"]?.ToString() ?? "",
obj["strictMode"]?.ToObject<bool>() ?? false,
obj["subnetOrdering"]?.ToString() ?? "",
obj["order"]?.ToObject<int>() ?? 0,
obj["editDate"]?.ToObject<DateTime?>(),
obj["showSubnet"]?.ToObject<bool>() ?? false,
obj["showVlan"]?.ToObject<bool>() ?? false,
obj["showVRF"]?.ToObject<bool>() ?? false,
obj["showSupernetOnly"]?.ToObject<bool>() ?? false,
obj["DNS"]?.ToObject<int>() ?? 0
);
private static Tag CreateTag(JObject obj) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["type"]?.ToString() ?? "",
obj["showtag"]?.ToObject<bool>() ?? false,
obj["bgcolor"]?.ToString() ?? "",
obj["fgcolor"]?.ToString() ?? "",
obj["compress"]?.ToString() ?? "",
obj["locked"]?.ToString() ?? "",
obj["updateTag"]?.ToObject<bool>() ?? false
);
private static Nameserver CreateNameserver(JObject obj) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["nameservers"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["permissions"]?.ToString() ?? "",
obj["editDate"]?.ToObject<DateTime?>()
);
private static Domain CreateDomain(JObject obj) => new(
obj["id"]?.ToObject<int>() ?? 0,
obj["name"]?.ToString() ?? "",
obj["description"]?.ToString() ?? "",
obj["sections"]?.ToString() ?? ""
);
#endregion
}

View File

@@ -0,0 +1,165 @@
namespace PS.IPAM.Helpers;
using System;
using System.Management.Automation;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
/// <summary>
/// Manages phpIPAM API sessions including creation, validation, and lifecycle.
/// </summary>
public static class SessionManager
{
private static Session? _currentSession;
/// <summary>
/// Gets or sets the current active session.
/// </summary>
public static Session? CurrentSession
{
get => _currentSession;
set => _currentSession = value;
}
/// <summary>
/// Tests the current session status.
/// </summary>
/// <returns>The session status indicating validity or issues.</returns>
public static SessionStatus GetSessionStatus()
{
if (_currentSession == null)
{
return SessionStatus.NoSession;
}
if (_currentSession.Expires == null)
{
return SessionStatus.Valid;
}
return _currentSession.Expires < DateTime.Now
? SessionStatus.Expired
: SessionStatus.Valid;
}
/// <summary>
/// Creates a new session using username/password credentials.
/// </summary>
/// <param name="url">The phpIPAM server URL.</param>
/// <param name="appId">The API application ID.</param>
/// <param name="credentials">The PowerShell credential object.</param>
/// <param name="ignoreSsl">Whether to ignore SSL certificate errors.</param>
/// <returns>The created session.</returns>
public static async Task<Session> CreateSessionWithCredentialsAsync(
string url,
string appId,
PSCredential credentials,
bool ignoreSsl = false)
{
var uri = $"{url}/api/{appId}/user";
var auth = Convert.ToBase64String(
Encoding.UTF8.GetBytes($"{credentials.UserName}:{GetPasswordString(credentials)}"));
using var client = CreateHttpClient(ignoreSsl);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth);
var response = await client.PostAsync(uri, null);
var content = await response.Content.ReadAsStringAsync();
var jsonResponse = JsonConvert.DeserializeObject<dynamic>(content);
if (jsonResponse?.success != true)
{
throw new InvalidOperationException(
jsonResponse?.error?.ToString() ?? "Failed to create session");
}
var token = jsonResponse.data.token.ToString();
var expires = DateTime.Parse(jsonResponse.data.expires.ToString());
_currentSession = new Session(
AuthType.Credentials,
token,
appId,
url,
expires,
credentials
);
return _currentSession;
}
/// <summary>
/// Creates a new session using a static API token.
/// </summary>
/// <param name="url">The phpIPAM server URL.</param>
/// <param name="appId">The API application ID.</param>
/// <param name="token">The API token.</param>
/// <returns>The created session.</returns>
public static Session CreateSessionWithToken(string url, string appId, string token)
{
_currentSession = new Session(
AuthType.Token,
token,
appId,
url,
null,
null
);
return _currentSession;
}
/// <summary>
/// Closes the current session and clears session data.
/// </summary>
public static void CloseSession()
{
_currentSession = null;
}
/// <summary>
/// Creates an HttpClient with optional SSL bypass.
/// </summary>
/// <param name="ignoreSsl">Whether to ignore SSL certificate errors.</param>
/// <param name="handler">Optional custom message handler for testing.</param>
/// <returns>A configured HttpClient instance.</returns>
public static HttpClient CreateHttpClient(bool ignoreSsl = false, HttpMessageHandler? handler = null)
{
if (handler != null)
{
return new HttpClient(handler);
}
if (ignoreSsl)
{
var sslHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
return new HttpClient(sslHandler);
}
return new HttpClient();
}
/// <summary>
/// Extracts the plain text password from a PSCredential object.
/// </summary>
private static string GetPasswordString(PSCredential credential)
{
var ptr = Marshal.SecureStringToBSTR(credential.Password);
try
{
return Marshal.PtrToStringBSTR(ptr);
}
finally
{
Marshal.ZeroFreeBSTR(ptr);
}
}
}

View File

@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// This file provides the IsExternalInit type required for using C# 9 records
// in projects targeting .NET Standard 2.1 or earlier.
#if !NET5_0_OR_GREATER
namespace System.Runtime.CompilerServices
{
using System.ComponentModel;
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
}
#endif

View File

@@ -1,77 +1,35 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
using System.Collections.Generic;
/// <summary>
/// Represents an IP address entry in phpIPAM.
/// </summary>
[Serializable] [Serializable]
public class Address { public sealed record Address(
public int Id { get; } int Id,
public int SubnetId { get; } int SubnetId,
public string Ip { get; } string Ip,
public bool IsGateway { get; } bool IsGateway,
public string Description { get; } string Description,
public string Hostname { get; } string Hostname,
public string MAC { get; } string MAC,
public string Owner { get; } string Owner,
public int TagId { get; } int TagId,
public int DeviceId { get; } int DeviceId,
public string Location { get; } string Location,
public string Port { get; } string Port,
public string Note { get; } string Note,
public DateTime? LastSeen { get; } DateTime? LastSeen,
public bool ExcludePing { get; } bool ExcludePing,
public bool PTRignore { get; } bool PTRIgnore,
public int PTR { get; }
public string FirewallAddressObject { get; }
public DateTime? EditDate { get; }
public int CustomerId { get; }
public Object? ExtendedData { get; }
public Address(
int id,
int subnetId,
string ip,
bool is_gateway,
string description,
string hostname,
string mac,
string owner,
int tag,
int deviceId,
string location,
string port,
string note,
DateTime? lastSeen,
bool excludePing,
bool PTRignore,
int PTR, int PTR,
string firewallAddressObject, string FirewallAddressObject,
DateTime? editDate, DateTime? EditDate,
int customer_id, int CustomerId,
Object? extendedData Dictionary<string, object>? ExtendedData = null
) { )
this.Id = id; {
this.SubnetId = subnetId; public override string ToString() => Ip;
this.Ip = ip;
this.IsGateway = is_gateway;
this.Description = description;
this.Hostname = hostname;
this.MAC = mac;
this.Owner = owner;
this.TagId = tag;
this.DeviceId = deviceId;
this.Location = location;
this.Port = port;
this.Note = note;
this.EditDate = lastSeen;
this.ExcludePing = excludePing;
this.PTRignore = PTRignore;
this.PTR = PTR;
this.FirewallAddressObject = firewallAddressObject;
this.EditDate = editDate;
this.CustomerId = customer_id;
this.ExtendedData = extendedData;
}
public override string ToString() {
return this.Ip;
}
} }

View File

@@ -1,27 +1,17 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
/// <summary>
/// Represents an L2 domain in phpIPAM.
/// </summary>
[Serializable] [Serializable]
public class Domain { public sealed record Domain(
public int Id { get; } int Id,
public string Name { get; } string Name,
public string Description { get; } string Description,
public string Sections { get; } string Sections
)
public Domain ( {
int id, public override string ToString() => Name;
string name,
string description,
string sections
) {
this.Id = id;
this.Name = name;
this.Description = description;
this.Sections = sections;
}
public override string ToString()
{
return this.Name;
}
} }

View File

@@ -1,8 +1,13 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
/// <summary>
/// Represents a nameserver configuration in phpIPAM.
/// </summary>
[Serializable] [Serializable]
public class Nameserver { public sealed record Nameserver
{
public int Id { get; } public int Id { get; }
public string Name { get; } public string Name { get; }
public string[] NameServers { get; } public string[] NameServers { get; }
@@ -16,13 +21,15 @@ public class Nameserver {
string nameServers, string nameServers,
string description, string description,
string permissions, string permissions,
DateTime? editDate DateTime? editDate)
) { {
this.Id = id; Id = id;
this.Name = name; Name = name;
this.NameServers = nameServers.Split(new char[] {';'}); NameServers = nameServers?.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
this.Description = description; Description = description;
this.Permissions = permissions; Permissions = permissions;
this.EditDate = editDate; EditDate = editDate;
} }
public override string ToString() => Name;
} }

View File

@@ -1,57 +1,27 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
/// <summary>
/// Represents a section in phpIPAM that organizes subnets.
/// </summary>
[Serializable] [Serializable]
public class Section { public sealed record Section(
public int Id { get; } int Id,
public string Name { get; } string Name,
public string Description { get; } string Description,
public int MasterSectionId { get; } int MasterSectionId,
public string Permissions { get; } string Permissions,
public bool StrictMode { get; } bool StrictMode,
public string SubnetOrdering { get; } string SubnetOrdering,
public int Order { get; } int Order,
public DateTime? EditDate { get; } DateTime? EditDate,
public bool ShowSubnet { get; } bool ShowSubnet,
public bool ShowVlan { get; } bool ShowVlan,
public bool ShowVRF { get; } bool ShowVRF,
public bool ShowSupernetOnly { get; } bool ShowSupernetOnly,
public int DNSId { get; } int DNSId
)
public Section ( {
int id, public override string ToString() => Name;
string name,
string description,
int masterSectionId,
string permissions,
bool strictMode,
string subnetOrdering,
int order,
DateTime? editDate,
bool showSubnet,
bool showVlan,
bool showVRF,
bool showSupernetOnly,
int dnsId
) {
this.Id = id;
this.Name = name;
this.Description = description;
this.MasterSectionId = masterSectionId;
this.Permissions = permissions;
this.StrictMode = strictMode;
this.SubnetOrdering = subnetOrdering;
this.Order = order;
this.EditDate = editDate;
this.ShowSubnet = showSubnet;
this.ShowVlan = showVlan;
this.ShowVRF = showVRF;
this.ShowSupernetOnly = showSupernetOnly;
this.DNSId = dnsId;
}
public override string ToString()
{
return this.Name;
}
} }

View File

@@ -1,32 +1,27 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
/// <summary>
/// Represents an authenticated session with phpIPAM API.
/// </summary>
[Serializable] [Serializable]
public class Session { public sealed record Session(
public AuthType AuthType { get; } AuthType AuthType,
public string Token { get; } string Token,
public string AppID { get; } string AppID,
public string URL { get; } string URL,
public DateTime? Expires { get; } DateTime? Expires,
public Object? Credentials { get; } object? Credentials
public Session( )
AuthType authType, {
string token, /// <summary>
string appId, /// Gets or sets the current authentication token.
string url, /// </summary>
DateTime? expires, public string Token { get; set; } = Token;
Object? credentials
) {
AuthType = authType;
Token = token;
AppID = appId;
URL = url;
Expires = expires;
Credentials = credentials;
}
public override string ToString() /// <summary>
{ /// Gets or sets the token expiration time.
return base.ToString(); /// </summary>
} public DateTime? Expires { get; set; } = Expires;
} }

View File

@@ -1,114 +1,51 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
using System.Collections.Generic;
/// <summary>
/// Represents a subnet/network in phpIPAM.
/// </summary>
[Serializable] [Serializable]
public class Subnetwork { public sealed record Subnetwork(
public int Id { get; } int Id,
public string Subnet { get; } string Subnet,
public int Mask { get; } int Mask,
public int SectionId { get; } int SectionId,
public string Description { get; } string Description,
public string LinkedSubnet { get; } string LinkedSubnet,
public string FirewallAddressObject { get; } string FirewallAddressObject,
public int VrfId { get; } int VrfId,
public int MasterSubnetId { get; } int MasterSubnetId,
public bool AllowRequests { get; } bool AllowRequests,
public int VlanId { get; } int VlanId,
public bool ShowName { get; } bool ShowName,
public int DeviceId { get; } int DeviceId,
public string Permissions { get; } string Permissions,
public bool PingSubnet { get; } bool PingSubnet,
public bool DiscoverSubnet { get; } bool DiscoverSubnet,
public bool ResolveDNS { get; } bool ResolveDNS,
public bool DNSRecursive { get; } bool DNSRecursive,
public bool DNSRecords { get; } bool DNSRecords,
public int NameserverId { get; } int NameserverId,
public bool ScanAgent { get; } bool ScanAgent,
public bool IsFolder { get; } bool IsFolder,
public bool IsFull { get; } bool IsFull,
public bool IsPool { get; } bool IsPool,
public int TagId { get; } int TagId,
public int Threshold { get; } int Threshold,
public int LocationId { get; } int LocationId,
public DateTime? EditDate { get; } DateTime? EditDate,
public DateTime? LastScan { get; } DateTime? LastScan,
public DateTime? LastDiscovery { get; } DateTime? LastDiscovery,
public Object Calculation { get; } object Calculation,
public Object? ExtendedData { get; } Dictionary<string, object>? ExtendedData = null
public Subnetwork( )
int id, {
string subnet, /// <summary>
int mask, /// Gets the subnet in CIDR notation (e.g., "192.168.1.0/24").
int sectionId, /// </summary>
string description, public string CIDR => $"{Subnet}/{Mask}";
string linkedSubnet,
string firewallAddressObject,
int vrfId,
int masterSubnetId,
bool allowRequests,
int vlanId,
bool showName,
int deviceId,
string permissions,
bool pingSubnet,
bool discoverSubnet,
bool resolveDNS,
bool dnsRecursive,
bool dnsRecords,
int nameserverId,
bool scanAgent,
bool isFolder,
bool isFull,
bool isPool,
int tagId,
int threshold,
int locationId,
DateTime? editDate,
DateTime? lastScan,
DateTime? lastDiscovery,
Object calculation,
Object? custom_fields
) {
this.Id = id;
this.Subnet = subnet;
this.Mask = mask;
this.SectionId = sectionId;
this.Description = description;
this.LinkedSubnet = linkedSubnet;
this.FirewallAddressObject = firewallAddressObject;
this.VrfId = vrfId;
this.MasterSubnetId = masterSubnetId;
this.AllowRequests = allowRequests;
this.VlanId = vlanId;
this.ShowName = showName;
this.DeviceId = deviceId;
this.Permissions = permissions;
this.PingSubnet = pingSubnet;
this.DiscoverSubnet = discoverSubnet;
this.ResolveDNS = resolveDNS;
this.DNSRecursive = dnsRecursive;
this.DNSRecords = dnsRecords;
this.NameserverId = nameserverId;
this.ScanAgent = scanAgent;
this.IsFolder = isFolder;
this.IsFull = isFull;
this.IsPool = isPool;
this.TagId = tagId;
this.Threshold = threshold;
this.LocationId = locationId;
this.EditDate = editDate;
this.LastScan = lastScan;
this.LastDiscovery = lastDiscovery;
this.Calculation = calculation;
this.ExtendedData = custom_fields;
}
public string GetCIDR() { public override string ToString() => CIDR;
return $"{this.Subnet}/{this.Mask}";
}
public override string ToString()
{
return this.GetCIDR();
}
} }

View File

@@ -1,45 +1,21 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
/// <summary>
/// Represents an address tag in phpIPAM.
/// </summary>
[Serializable] [Serializable]
public class Tag { public sealed record Tag(
public int Id { get; } int Id,
public string Type { get; } string Type,
public bool ShowTag { get; } bool ShowTag,
public string BGColor { get; } string BackgroundColor,
public string FGColor { get; } string ForegroundColor,
public bool Compress { get; } string Compress,
public bool Locked { get; } string Locked,
public bool UpdateTag { get; } bool UpdateTag
)
public Tag( {
int id, public override string ToString() => Type;
string type,
bool showTag,
string BGColor,
string FGColor,
string compress,
string locked,
bool updateTag
) {
this.Id = id;
this.Type = type;
this.ShowTag = showTag;
this.BGColor = BGColor;
this.FGColor = FGColor;
this.Compress = this.StringToBool(compress);
this.Locked = this.StringToBool(locked);
this.UpdateTag = updateTag;
}
public override string ToString() {
return this.Type;
}
private bool StringToBool(string str) {
if (str == "Yes") {
return true;
}
return false;
}
} }

View File

@@ -1,39 +1,27 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
using System.Dynamic; using System.Collections.Generic;
/// <summary>
/// Represents a VLAN in phpIPAM.
/// </summary>
[Serializable] [Serializable]
public class Vlan : DynamicObject { public sealed record Vlan(
public int Id { get; } int Id,
public int DomainId { get; } int DomainId,
public string Name { get; } string Name,
public int Number { get; } int Number,
public string Description { get; } string Description,
public DateTime? EditDate { get; } DateTime? EditDate,
public int CustomerId { get; } int CustomerId,
public Object? ExtendedData { get; } Dictionary<string, object>? ExtendedData = null
public Vlan ( )
int vlanId, {
int domainId, /// <summary>
string name, /// Alias for Id to maintain API compatibility.
int number, /// </summary>
string description, public int VlanId => Id;
DateTime? editDate,
int customer_id,
Object? custom_fields
) {
this.Id = vlanId;
this.DomainId = domainId;
this.Name = name;
this.Number = number;
this.Description = description;
this.EditDate = editDate;
this.CustomerId = customer_id;
this.ExtendedData = custom_fields;
}
public override string ToString() public override string ToString() => Number.ToString();
{
return $"{this.Number}";
}
} }

View File

@@ -1,35 +1,21 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
using System.Collections.Generic;
/// <summary>
/// Represents a VRF (Virtual Routing and Forwarding) instance in phpIPAM.
/// </summary>
[Serializable] [Serializable]
public class Vrf { public sealed record Vrf(
public int Id { get; } int Id,
public string Name { get; } string Name,
public string RouteDistinguisher { get; } string RouteDistinguisher,
public string Description { get; } string Description,
public string Sections { get; } string Sections,
public DateTime? EditDate { get; } DateTime? EditDate,
public Object? ExtendedData { get; } Dictionary<string, object>? ExtendedData = null
)
public Vrf( {
int id, public override string ToString() => Name;
string name,
string rd,
string description,
string sections,
DateTime? editDate,
Object? custom_fields
) {
this.Id = id;
this.Name = name;
this.RouteDistinguisher = rd;
this.Description = description;
this.Sections = sections;
this.EditDate = editDate;
this.ExtendedData = custom_fields;
}
public override string ToString()
{
return this.Name;
}
} }

View File

@@ -5,6 +5,14 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<AssemblyName>ps.ipam</AssemblyName>
<RootNamespace>PS.IPAM</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,22 @@
namespace PS.IPAM;
/// <summary>
/// Represents the current status of an API session.
/// </summary>
public enum SessionStatus
{
/// <summary>
/// No session exists or no token is available.
/// </summary>
NoSession,
/// <summary>
/// The session token has expired.
/// </summary>
Expired,
/// <summary>
/// The session is valid and ready for use.
/// </summary>
Valid
}

View File

@@ -1,8 +1,20 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
/// <summary>
/// Specifies the authentication method for phpIPAM API.
/// </summary>
[Serializable] [Serializable]
public enum AuthType { public enum AuthType
credentials, {
token /// <summary>
/// Authentication using username and password credentials.
/// </summary>
Credentials,
/// <summary>
/// Authentication using a static API token.
/// </summary>
Token
} }

View File

@@ -1,14 +1,19 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
/// <summary>
/// Represents the main API controllers in phpIPAM.
/// </summary>
[Serializable] [Serializable]
public enum controllers { public enum ApiController
user, {
vlan, User,
subnets, Vlan,
addresses, Subnets,
sections, Addresses,
vrf, Sections,
l2domains, Vrf,
tools L2Domains,
Tools
} }

View File

@@ -1,16 +1,21 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
/// <summary>
/// Represents sub-controllers/endpoints within main API controllers.
/// </summary>
[Serializable] [Serializable]
public enum subcontrollers { public enum ApiSubController
nameservers, {
tags, Nameservers,
devices, Tags,
device_types, Devices,
vlans, DeviceTypes,
vrfs, Vlans,
scanagents, Vrfs,
locations, ScanAgents,
nat, Locations,
racks Nat,
Racks
} }

View File

@@ -1,8 +1,13 @@
namespace PS.IPAM; namespace PS.IPAM;
using System; using System;
/// <summary>
/// Represents the model types returned by the phpIPAM API.
/// </summary>
[Serializable] [Serializable]
public enum types { public enum ModelType
{
Address, Address,
Domain, Domain,
Section, Section,

View File

@@ -47,16 +47,28 @@ function New-Address {
Port. Port.
#> #>
[OutputType([PS.IPAM.address])] [OutputType([PS.IPAM.address])]
[CmdletBinding()] [CmdletBinding(DefaultParameterSetName="BySubnetId")]
param ( param (
[parameter( [parameter(
Mandatory=$true, Mandatory=$true,
ValueFromPipeline=$true, ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true, ValueFromPipelineByPropertyName=$true,
Position=0)] Position=0,
ParameterSetName="BySubnetId"
)]
[ValidateNotNullOrEmpty()] [ValidateNotNullOrEmpty()]
[int] [int]
$SubnetId, $SubnetId,
[Parameter(
Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
Position=0,
ParameterSetName="BySubnetObject"
)]
[ValidateNotNullOrEmpty()]
[ps.ipam.subnetwork]
$SubnetObject,
[parameter( [parameter(
Mandatory=$true, Mandatory=$true,
ValueFromPipeline=$true, ValueFromPipeline=$true,
@@ -175,8 +187,19 @@ function New-Address {
Method = "POST" Method = "POST"
} }
switch ($PSCmdlet.ParameterSetName) {
"BySubnetId" {
$_subnetId = $SubnetId
break
}
"BySubnetObject" {
$_subnetId = $SubnetObject.id
break
}
}
$_body = @{ $_body = @{
subnetId = $SubnetId subnetId = $_subnetId
ip = $Ip ip = $Ip
} }
if ($Gateway) { $_body.Add("is_gateway", "1") } if ($Gateway) { $_body.Add("is_gateway", "1") }

View File

@@ -6,7 +6,7 @@
# #
@{ @{
RootModule = 'ps.ipam.psm1' RootModule = 'ps.ipam.dll'
ModuleVersion = '2.0' ModuleVersion = '2.0'
GUID = 'cd573493-4245-4073-a238-fab2251d78d0' GUID = 'cd573493-4245-4073-a238-fab2251d78d0'
Author = 'Nikolay Tatarinov' Author = 'Nikolay Tatarinov'
@@ -15,14 +15,14 @@
PowerShellVersion = '5.1' PowerShellVersion = '5.1'
RequiredAssemblies = @( RequiredAssemblies = @(
'classlib\bin\Release\netstandard2.1\publish\classlib.dll' 'ps.ipam.dll'
) )
TypesToProcess = @( TypesToProcess = @(
'types\types.ps1xml' 'types\types.ps1xml'
) )
FunctionsToExport = @( CmdletsToExport = @(
'Assign-Tag', 'Assign-Tag',
'Close-Session', 'Close-Session',
'New-Session', 'New-Session',
@@ -41,6 +41,7 @@
'New-Address', 'New-Address',
'New-FirstFreeIP', 'New-FirstFreeIP',
'Set-Address', 'Set-Address',
'Set-Subnet',
'Remove-Address' 'Remove-Address'
) )
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.