From df30851a3a9124a46fba7c367f543e96fe0e4d7c Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 17:01:30 +0300 Subject: [PATCH 1/4] Update Gitea CI workflow to restore class library and test project dependencies separately --- .gitea/workflows/ci.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 1e86b2a..2ceed50 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -26,8 +26,11 @@ jobs: with: dotnet-version: '8.0.x' - - name: Restore dependencies - run: dotnet restore + - 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 From 694822f0d6482fbb4ab534d4c45ea9226f8ad09d Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 17:04:22 +0300 Subject: [PATCH 2/4] Update Gitea CI workflow to downgrade upload and download artifact actions from v4 to v3 --- .gitea/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 2ceed50..41645d3 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -42,7 +42,7 @@ jobs: 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@v4 + uses: actions/upload-artifact@v3 if: always() with: name: test-results @@ -89,7 +89,7 @@ jobs: cp -r images output/ps.ipam/ - name: Upload module artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: ps.ipam-module path: output/ps.ipam/ @@ -102,7 +102,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') steps: - name: Download module artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 with: name: ps.ipam-module path: ps.ipam From f56784f2aaf85c0dac091ee8e032bd5565f412b2 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 17:25:18 +0300 Subject: [PATCH 3/4] 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. --- .../Cmdlets/GetAddressCmdletTests.cs | 45 +- .../Cmdlets/NewSessionCmdletTests.cs | 9 +- classlib.tests/Helpers/RequestHelperTests.cs | 60 +- classlib.tests/Helpers/SessionManagerTests.cs | 34 +- classlib.tests/Models/AddressTests.cs | 22 +- classlib.tests/Models/NameserverTests.cs | 30 +- classlib.tests/Models/SessionTests.cs | 24 +- classlib.tests/Models/SubnetworkTests.cs | 27 +- classlib.tests/Models/TagTests.cs | 32 +- classlib/Cmdlets/AssignTagCmdlet.cs | 35 +- classlib/Cmdlets/CloseSessionCmdlet.cs | 18 +- classlib/Cmdlets/GetAddressCmdlet.cs | 73 +- classlib/Cmdlets/GetFirstFreeIPCmdlet.cs | 69 +- classlib/Cmdlets/GetL2DomainCmdlet.cs | 32 +- classlib/Cmdlets/GetNameserverCmdlet.cs | 32 +- classlib/Cmdlets/GetPermissionsCmdlet.cs | 69 +- classlib/Cmdlets/GetSectionCmdlet.cs | 35 +- classlib/Cmdlets/GetSubnetCmdlet.cs | 220 ++++--- classlib/Cmdlets/GetSubnetUsageCmdlet.cs | 76 ++- classlib/Cmdlets/GetTagCmdlet.cs | 38 +- classlib/Cmdlets/GetVlanCmdlet.cs | 81 +-- classlib/Cmdlets/GetVrfCmdlet.cs | 32 +- classlib/Cmdlets/NewAddressCmdlet.cs | 199 ++---- classlib/Cmdlets/NewFirstFreeIPCmdlet.cs | 178 ++--- classlib/Cmdlets/NewSessionCmdlet.cs | 33 +- classlib/Cmdlets/NewSubnetCmdlet.cs | 214 ++---- classlib/Cmdlets/RemoveAddressCmdlet.cs | 52 +- classlib/Cmdlets/SetAddressCmdlet.cs | 166 ++--- classlib/Cmdlets/SetSubnetCmdlet.cs | 151 ++--- classlib/Helpers/RequestHelper.cs | 623 ++++++++++-------- classlib/Helpers/SessionManager.cs | 100 ++- classlib/class/address.cs | 106 +-- classlib/class/domain.cs | 36 +- classlib/class/nameserver.cs | 27 +- classlib/class/section.cs | 76 +-- classlib/class/session.cs | 50 +- classlib/class/subnet.cs | 155 ++--- classlib/class/tag.cs | 58 +- classlib/class/vlan.cs | 58 +- classlib/class/vrf.cs | 48 +- classlib/enum/authType.cs | 20 +- classlib/enum/controllers.cs | 25 +- classlib/enum/subcontrollers.cs | 29 +- classlib/enum/types.cs | 9 +- 44 files changed, 1601 insertions(+), 1905 deletions(-) diff --git a/classlib.tests/Cmdlets/GetAddressCmdletTests.cs b/classlib.tests/Cmdlets/GetAddressCmdletTests.cs index 8ca88e5..273a575 100644 --- a/classlib.tests/Cmdlets/GetAddressCmdletTests.cs +++ b/classlib.tests/Cmdlets/GetAddressCmdletTests.cs @@ -109,6 +109,13 @@ public class GetAddressCmdletTests : IDisposable cmdlet.SubnetCIDR.Should().Be("192.168.1.0/24"); } + [Fact] + public void GetAddressCmdlet_InheritsFromBaseCmdlet() + { + var cmdlet = new GetAddressCmdlet(); + cmdlet.Should().BeAssignableTo(); + } + // Test the underlying RequestHelper functionality that the cmdlet uses [Fact] @@ -129,8 +136,8 @@ public class GetAddressCmdletTests : IDisposable // Act var result = await RequestHelper.InvokeRequest( "GET", - controllers.addresses, - types.Address, + ApiController.Addresses, + ModelType.Address, null, null, new[] { "1" } @@ -155,8 +162,8 @@ public class GetAddressCmdletTests : IDisposable // Act await RequestHelper.InvokeRequest( "GET", - controllers.addresses, - types.Address, + ApiController.Addresses, + ModelType.Address, null, null, new[] { "42" } @@ -177,8 +184,8 @@ public class GetAddressCmdletTests : IDisposable // Act await RequestHelper.InvokeRequest( "GET", - controllers.addresses, - types.Address, + ApiController.Addresses, + ModelType.Address, null, null, new[] { "search", "192.168.1.50" } @@ -199,8 +206,8 @@ public class GetAddressCmdletTests : IDisposable // Act await RequestHelper.InvokeRequest( "GET", - controllers.addresses, - types.Address, + ApiController.Addresses, + ModelType.Address, null, null, new[] { "search_hostname", "myserver.example.com" } @@ -221,8 +228,8 @@ public class GetAddressCmdletTests : IDisposable // Act await RequestHelper.InvokeRequest( "GET", - controllers.subnets, - types.Address, + ApiController.Subnets, + ModelType.Address, null, null, new[] { "10", "addresses" } @@ -243,8 +250,8 @@ public class GetAddressCmdletTests : IDisposable // Act await RequestHelper.InvokeRequest( "GET", - controllers.addresses, - types.Address, + ApiController.Addresses, + ModelType.Address, null, null, new[] { "tags", "2", "addresses" } @@ -269,8 +276,8 @@ public class GetAddressCmdletTests : IDisposable // Act var result = await RequestHelper.InvokeRequest( "GET", - controllers.subnets, - types.Address, + ApiController.Subnets, + ModelType.Address, null, null, new[] { "10", "addresses" } @@ -294,8 +301,8 @@ public class GetAddressCmdletTests : IDisposable // Act var result = await RequestHelper.InvokeRequest( "GET", - controllers.addresses, - types.Address, + ApiController.Addresses, + ModelType.Address, null, null, new[] { "999" } @@ -313,11 +320,11 @@ public class GetAddressCmdletTests : IDisposable // Act var action = async () => await RequestHelper.InvokeRequest( "GET", - controllers.addresses, - types.Address + ApiController.Addresses, + ModelType.Address ); // Assert - await action.Should().ThrowAsync().WithMessage("No session available!"); + await action.Should().ThrowAsync().WithMessage("No session available!"); } } diff --git a/classlib.tests/Cmdlets/NewSessionCmdletTests.cs b/classlib.tests/Cmdlets/NewSessionCmdletTests.cs index 64c8f71..99cfb6c 100644 --- a/classlib.tests/Cmdlets/NewSessionCmdletTests.cs +++ b/classlib.tests/Cmdlets/NewSessionCmdletTests.cs @@ -93,11 +93,18 @@ public class NewSessionCmdletTests : IDisposable 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.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(); + } } diff --git a/classlib.tests/Helpers/RequestHelperTests.cs b/classlib.tests/Helpers/RequestHelperTests.cs index 90616a0..7539339 100644 --- a/classlib.tests/Helpers/RequestHelperTests.cs +++ b/classlib.tests/Helpers/RequestHelperTests.cs @@ -27,9 +27,9 @@ public class RequestHelperTests : IDisposable _mockHandler?.ForceDispose(); } - private void SetupSession(AuthType authType = AuthType.token) + private void SetupSession(AuthType authType = AuthType.Token) { - if (authType == AuthType.token) + if (authType == AuthType.Token) { SessionManager.CreateSessionWithToken("https://ipam.example.com", "testapp", "test-token"); } @@ -37,7 +37,7 @@ public class RequestHelperTests : IDisposable { // For credentials auth, we need to set up a session manually with future expiry var session = new Session( - AuthType.credentials, + AuthType.Credentials, "cred-token", "testapp", "https://ipam.example.com", @@ -61,10 +61,10 @@ public class RequestHelperTests : IDisposable // Arrange - no session set up // Act - var action = async () => await RequestHelper.InvokeRequest("GET", controllers.addresses); + var action = async () => await RequestHelper.InvokeRequest("GET", ApiController.Addresses); // Assert - await action.Should().ThrowAsync().WithMessage("No session available!"); + await action.Should().ThrowAsync().WithMessage("No session available!"); } [Fact] @@ -76,7 +76,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse("{\"id\":1,\"ip\":\"192.168.1.1\"}"); // Act - await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "1" }); + await RequestHelper.InvokeRequest("GET", ApiController.Addresses, ModelType.Address, null, null, new[] { "1" }); // Assert handler.LastRequest.Should().NotBeNull(); @@ -92,7 +92,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse("[]"); // Act - await RequestHelper.InvokeRequest("GET", controllers.subnets, null, subcontrollers.tags, null, new[] { "1" }); + await RequestHelper.InvokeRequest("GET", ApiController.Subnets, null, ApiSubController.Tags, null, new[] { "1" }); // Assert handler.LastRequest!.RequestUri!.ToString().Should().Contain("/subnets/tags/1/"); @@ -102,12 +102,12 @@ public class RequestHelperTests : IDisposable public async Task InvokeRequest_WithTokenAuth_AddsPhpipamTokenHeader() { // Arrange - SetupSession(AuthType.token); + SetupSession(AuthType.Token); var handler = SetupMockHandler(); handler.WithSuccessResponse("{}"); // Act - await RequestHelper.InvokeRequest("GET", controllers.addresses); + await RequestHelper.InvokeRequest("GET", ApiController.Addresses); // Assert handler.GetLastRequestHeader("phpipam-token").Should().Be("test-token"); @@ -117,12 +117,12 @@ public class RequestHelperTests : IDisposable public async Task InvokeRequest_WithCredentialsAuth_AddsTokenHeader() { // Arrange - SetupSession(AuthType.credentials); + SetupSession(AuthType.Credentials); var handler = SetupMockHandler(); handler.WithSuccessResponse("{}"); // Act - await RequestHelper.InvokeRequest("GET", controllers.addresses); + await RequestHelper.InvokeRequest("GET", ApiController.Addresses); // Assert handler.GetLastRequestHeader("token").Should().Be("cred-token"); @@ -137,7 +137,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse("{}"); // Act - await RequestHelper.InvokeRequest("GET", controllers.addresses); + await RequestHelper.InvokeRequest("GET", ApiController.Addresses); // Assert handler.LastRequest!.Method.Should().Be(HttpMethod.Get); @@ -152,7 +152,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse("{\"id\":1}"); // Act - await RequestHelper.InvokeRequest("POST", controllers.addresses, null, null, new { ip = "10.0.0.1" }); + await RequestHelper.InvokeRequest("POST", ApiController.Addresses, null, null, new { ip = "10.0.0.1" }); // Assert handler.LastRequest!.Method.Should().Be(HttpMethod.Post); @@ -167,7 +167,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse("{\"id\":1}"); // Act - await RequestHelper.InvokeRequest("PATCH", controllers.addresses, null, null, new { description = "updated" }, new[] { "1" }); + await RequestHelper.InvokeRequest("PATCH", ApiController.Addresses, null, null, new { description = "updated" }, new[] { "1" }); // Assert handler.LastRequest!.Method.Should().Be(new HttpMethod("PATCH")); @@ -182,7 +182,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse("{}"); // Act - await RequestHelper.InvokeRequest("DELETE", controllers.addresses, null, null, null, new[] { "1" }); + await RequestHelper.InvokeRequest("DELETE", ApiController.Addresses, null, null, null, new[] { "1" }); // Assert handler.LastRequest!.Method.Should().Be(HttpMethod.Delete); @@ -197,7 +197,7 @@ public class RequestHelperTests : IDisposable handler.WithNotFoundResponse(); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "999" }); + var result = await RequestHelper.InvokeRequest("GET", ApiController.Addresses, ModelType.Address, null, null, new[] { "999" }); // Assert result.Should().BeNull(); @@ -212,7 +212,7 @@ public class RequestHelperTests : IDisposable handler.WithErrorResponse(HttpStatusCode.InternalServerError, "Server error"); // Act - var action = async () => await RequestHelper.InvokeRequest("GET", controllers.addresses); + var action = async () => await RequestHelper.InvokeRequest("GET", ApiController.Addresses); // Assert await action.Should().ThrowAsync(); @@ -249,7 +249,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse(addressJson); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "1" }); + var result = await RequestHelper.InvokeRequest("GET", ApiController.Addresses, ModelType.Address, null, null, new[] { "1" }); // Assert result.Should().BeOfType
(); @@ -272,7 +272,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse(addressArrayJson); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.subnets, types.Address, null, null, new[] { "10", "addresses" }); + var result = await RequestHelper.InvokeRequest("GET", ApiController.Subnets, ModelType.Address, null, null, new[] { "10", "addresses" }); // Assert result.Should().BeAssignableTo>(); @@ -300,7 +300,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse(vlanJson); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.vlan, types.Vlan, null, null, new[] { "100" }); + var result = await RequestHelper.InvokeRequest("GET", ApiController.Vlan, ModelType.Vlan, null, null, new[] { "100" }); // Assert result.Should().BeOfType(); @@ -351,14 +351,14 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse(subnetJson); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.subnets, types.Subnetwork, null, null, new[] { "1" }); + var result = await RequestHelper.InvokeRequest("GET", ApiController.Subnets, ModelType.Subnetwork, null, null, new[] { "1" }); // Assert result.Should().BeOfType(); var subnet = (Subnetwork)result!; subnet.Subnet.Should().Be("192.168.1.0"); subnet.Mask.Should().Be(24); - subnet.GetCIDR().Should().Be("192.168.1.0/24"); + subnet.CIDR.Should().Be("192.168.1.0/24"); } [Fact] @@ -386,7 +386,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse(sectionJson); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.sections, types.Section, null, null, new[] { "1" }); + var result = await RequestHelper.InvokeRequest("GET", ApiController.Sections, ModelType.Section, null, null, new[] { "1" }); // Assert result.Should().BeOfType
(); @@ -411,12 +411,12 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse(addressJson); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "1" }); + 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 = (Dictionary)address.ExtendedData!; + var extendedData = address.ExtendedData!; extendedData.Should().ContainKey("custom_environment"); extendedData.Should().ContainKey("custom_owner"); } @@ -430,7 +430,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse(@"{""some"": ""data""}"); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.tools); + var result = await RequestHelper.InvokeRequest("GET", ApiController.Tools); // Assert result.Should().NotBeNull(); @@ -447,7 +447,7 @@ public class RequestHelperTests : IDisposable var parameters = new { ip = "10.0.0.1", subnetId = 5, description = "New address" }; // Act - await RequestHelper.InvokeRequest("POST", controllers.addresses, null, null, parameters); + await RequestHelper.InvokeRequest("POST", ApiController.Addresses, null, null, parameters); // Assert handler.LastRequest!.Content.Should().NotBeNull(); @@ -465,7 +465,7 @@ public class RequestHelperTests : IDisposable handler.WithSuccessResponse("{}"); // Act - await RequestHelper.InvokeRequest("GET", controllers.addresses); + await RequestHelper.InvokeRequest("GET", ApiController.Addresses); // Assert handler.LastRequest!.Headers.Accept.Should().Contain(h => h.MediaType == "application/json"); @@ -480,7 +480,7 @@ public class RequestHelperTests : IDisposable handler.WithResponse(HttpStatusCode.OK, "", "application/json"); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.addresses); + var result = await RequestHelper.InvokeRequest("GET", ApiController.Addresses); // Assert result.Should().BeNull(); @@ -495,7 +495,7 @@ public class RequestHelperTests : IDisposable handler.WithJsonResponse(@"{""code"": 200, ""success"": true, ""data"": null}"); // Act - var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address); + var result = await RequestHelper.InvokeRequest("GET", ApiController.Addresses, ModelType.Address); // Assert result.Should().BeNull(); diff --git a/classlib.tests/Helpers/SessionManagerTests.cs b/classlib.tests/Helpers/SessionManagerTests.cs index d521863..cb15994 100644 --- a/classlib.tests/Helpers/SessionManagerTests.cs +++ b/classlib.tests/Helpers/SessionManagerTests.cs @@ -21,60 +21,60 @@ public class SessionManagerTests : IDisposable } [Fact] - public void TestSession_WhenNoSession_ReturnsNoToken() + public void GetSessionStatus_WhenNoSession_ReturnsNoSession() { // Arrange SessionManager.CurrentSession = null; // Act - var result = SessionManager.TestSession(); + var result = SessionManager.GetSessionStatus(); // Assert - result.Should().Be("NoToken"); + result.Should().Be(SessionStatus.NoSession); } [Fact] - public void TestSession_WhenSessionWithNullExpires_ReturnsValid() + public void GetSessionStatus_WhenSessionWithNullExpires_ReturnsValid() { // Arrange - var session = new Session(AuthType.token, "test-token", "app", "https://test.com", null, null); + var session = new Session(AuthType.Token, "test-token", "app", "https://test.com", null, null); SessionManager.CurrentSession = session; // Act - var result = SessionManager.TestSession(); + var result = SessionManager.GetSessionStatus(); // Assert - result.Should().Be("Valid"); + result.Should().Be(SessionStatus.Valid); } [Fact] - public void TestSession_WhenSessionNotExpired_ReturnsValid() + 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); + var session = new Session(AuthType.Credentials, "test-token", "app", "https://test.com", futureExpiry, null); SessionManager.CurrentSession = session; // Act - var result = SessionManager.TestSession(); + var result = SessionManager.GetSessionStatus(); // Assert - result.Should().Be("Valid"); + result.Should().Be(SessionStatus.Valid); } [Fact] - public void TestSession_WhenSessionExpired_ReturnsExpired() + 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); + var session = new Session(AuthType.Credentials, "test-token", "app", "https://test.com", pastExpiry, null); SessionManager.CurrentSession = session; // Act - var result = SessionManager.TestSession(); + var result = SessionManager.GetSessionStatus(); // Assert - result.Should().Be("Expired"); + result.Should().Be(SessionStatus.Expired); } [Fact] @@ -93,7 +93,7 @@ public class SessionManagerTests : IDisposable session.URL.Should().Be(url); session.AppID.Should().Be(appId); session.Token.Should().Be(token); - session.AuthType.Should().Be(AuthType.token); + session.AuthType.Should().Be(AuthType.Token); session.Expires.Should().BeNull(); session.Credentials.Should().BeNull(); } @@ -144,7 +144,7 @@ public class SessionManagerTests : IDisposable public void CurrentSession_CanBeSetDirectly() { // Arrange - var session = new Session(AuthType.token, "token", "app", "https://test.com", null, null); + var session = new Session(AuthType.Token, "token", "app", "https://test.com", null, null); // Act SessionManager.CurrentSession = session; diff --git a/classlib.tests/Models/AddressTests.cs b/classlib.tests/Models/AddressTests.cs index 60ec426..72edfd1 100644 --- a/classlib.tests/Models/AddressTests.cs +++ b/classlib.tests/Models/AddressTests.cs @@ -55,7 +55,7 @@ public class AddressTests address.Note.Should().Be(note); address.LastSeen.Should().Be(lastSeen); address.ExcludePing.Should().Be(excludePing); - address.PTRignore.Should().Be(ptrIgnore); + address.PTRIgnore.Should().Be(ptrIgnore); address.PTR.Should().Be(ptr); address.FirewallAddressObject.Should().Be(firewallObject); address.EditDate.Should().Be(editDate); @@ -113,4 +113,24 @@ public class AddressTests // 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); + } } diff --git a/classlib.tests/Models/NameserverTests.cs b/classlib.tests/Models/NameserverTests.cs index 570cd9f..def212c 100644 --- a/classlib.tests/Models/NameserverTests.cs +++ b/classlib.tests/Models/NameserverTests.cs @@ -57,7 +57,7 @@ public class NameserverTests } [Fact] - public void Constructor_WithEmptyNameservers_ReturnsArrayWithEmptyString() + public void Constructor_WithEmptyNameservers_ReturnsEmptyArray() { // Arrange var nameServersString = ""; @@ -65,9 +65,8 @@ public class NameserverTests // Act var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null); - // Assert - nameserver.NameServers.Should().HaveCount(1); - nameserver.NameServers[0].Should().BeEmpty(); + // Assert - Empty entries are excluded + nameserver.NameServers.Should().BeEmpty(); } [Fact] @@ -79,4 +78,27 @@ public class NameserverTests // 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"); + } } diff --git a/classlib.tests/Models/SessionTests.cs b/classlib.tests/Models/SessionTests.cs index 4d9ebb2..79fa200 100644 --- a/classlib.tests/Models/SessionTests.cs +++ b/classlib.tests/Models/SessionTests.cs @@ -10,7 +10,7 @@ public class SessionTests public void Constructor_WithCredentialsAuth_SetsAllProperties() { // Arrange - var authType = AuthType.credentials; + var authType = AuthType.Credentials; var token = "test-token-123"; var appId = "myApp"; var url = "https://ipam.example.com"; @@ -21,7 +21,7 @@ public class SessionTests var session = new Session(authType, token, appId, url, expires, credentials); // Assert - session.AuthType.Should().Be(AuthType.credentials); + session.AuthType.Should().Be(AuthType.Credentials); session.Token.Should().Be(token); session.AppID.Should().Be(appId); session.URL.Should().Be(url); @@ -33,7 +33,7 @@ public class SessionTests public void Constructor_WithTokenAuth_SetsAllProperties() { // Arrange - var authType = AuthType.token; + var authType = AuthType.Token; var token = "static-api-token"; var appId = "apiApp"; var url = "https://ipam.test.com"; @@ -42,7 +42,7 @@ public class SessionTests var session = new Session(authType, token, appId, url, null, null); // Assert - session.AuthType.Should().Be(AuthType.token); + session.AuthType.Should().Be(AuthType.Token); session.Token.Should().Be(token); session.AppID.Should().Be(appId); session.URL.Should().Be(url); @@ -54,7 +54,7 @@ public class SessionTests public void Token_CanBeModified() { // Arrange - var session = new Session(AuthType.credentials, "old-token", "app", "https://test.com", null, null); + var session = new Session(AuthType.Credentials, "old-token", "app", "https://test.com", null, null); // Act session.Token = "new-token"; @@ -67,7 +67,7 @@ public class SessionTests public void Expires_CanBeModified() { // Arrange - var session = new Session(AuthType.credentials, "token", "app", "https://test.com", null, null); + var session = new Session(AuthType.Credentials, "token", "app", "https://test.com", null, null); var newExpiry = new DateTime(2027, 1, 1); // Act @@ -78,15 +78,13 @@ public class SessionTests } [Fact] - public void ToString_ReturnsDefaultObjectString() + public void Record_Equality_WorksForSameValues() { // Arrange - var session = new Session(AuthType.token, "token", "app", "https://test.com", null, null); + 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); - // Act - var result = session.ToString(); - - // Assert - result.Should().Contain("PS.IPAM.Session"); + // Assert - Records use value equality + session1.Should().Be(session2); } } diff --git a/classlib.tests/Models/SubnetworkTests.cs b/classlib.tests/Models/SubnetworkTests.cs index eca5166..bc3ec51 100644 --- a/classlib.tests/Models/SubnetworkTests.cs +++ b/classlib.tests/Models/SubnetworkTests.cs @@ -93,13 +93,13 @@ public class SubnetworkTests [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 GetCIDR_ReturnsCidrNotation(string subnet, int mask, string expectedCidr) + public void CIDR_ReturnsCidrNotation(string subnet, int mask, string expectedCidr) { // Arrange var subnetwork = CreateSubnetwork(subnet, mask); // Act - var result = subnetwork.GetCIDR(); + var result = subnetwork.CIDR; // Assert result.Should().Be(expectedCidr); @@ -119,16 +119,31 @@ public class SubnetworkTests } [Fact] - public void ToString_EqualsGetCIDR() + public void ToString_EqualsCIDRProperty() { // Arrange var subnetwork = CreateSubnetwork("172.20.0.0", 12); // Act & Assert - subnetwork.ToString().Should().Be(subnetwork.GetCIDR()); + subnetwork.ToString().Should().Be(subnetwork.CIDR); } - private static Subnetwork CreateSubnetwork(string subnet, int mask) + [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, "", "", "", @@ -136,7 +151,7 @@ public class SubnetworkTests "", false, false, false, false, false, 0, false, false, false, false, 0, 0, 0, null, null, null, - new object(), null + calculation ?? new object(), null ); } } diff --git a/classlib.tests/Models/TagTests.cs b/classlib.tests/Models/TagTests.cs index db345ef..a40010b 100644 --- a/classlib.tests/Models/TagTests.cs +++ b/classlib.tests/Models/TagTests.cs @@ -26,28 +26,13 @@ public class TagTests tag.Id.Should().Be(id); tag.Type.Should().Be(type); tag.ShowTag.Should().Be(showTag); - tag.BGColor.Should().Be(bgColor); - tag.FGColor.Should().Be(fgColor); - tag.Compress.Should().BeTrue(); - tag.Locked.Should().BeFalse(); + 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("Yes", true)] - [InlineData("No", false)] - [InlineData("", false)] - [InlineData("yes", false)] // Case sensitive - [InlineData("true", false)] // Only "Yes" is true - public void StringToBool_ConvertsCorrectly(string input, bool expected) - { - // The StringToBool is private, so we test through the constructor - // Using Compress field which uses StringToBool - var tag = new Tag(1, "Test", false, "", "", input, "No", false); - - tag.Compress.Should().Be(expected); - } - [Theory] [InlineData("Used")] [InlineData("Available")] @@ -66,12 +51,13 @@ public class TagTests } [Fact] - public void Locked_WithYes_IsTrue() + public void Record_Equality_WorksCorrectly() { - // Arrange & Act - var tag = new Tag(1, "Test", false, "", "", "No", "Yes", false); + // 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 - tag.Locked.Should().BeTrue(); + tag1.Should().Be(tag2); } } diff --git a/classlib/Cmdlets/AssignTagCmdlet.cs b/classlib/Cmdlets/AssignTagCmdlet.cs index 5ac2a64..a506450 100644 --- a/classlib/Cmdlets/AssignTagCmdlet.cs +++ b/classlib/Cmdlets/AssignTagCmdlet.cs @@ -1,24 +1,30 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; -[Cmdlet("Assign", "Tag")] -public class AssignTagCmdlet : PSCmdlet +/// +/// Assigns a tag to an address in phpIPAM. +/// +[Cmdlet("Assign", "Tag", SupportsShouldProcess = true)] +[OutputType(typeof(Address))] +public class AssignTagCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, - Position = 0)] + Position = 0, + HelpMessage = "The address to assign the tag to.")] [ValidateNotNullOrEmpty] public Address? AddressObject { get; set; } [Parameter( Mandatory = true, - Position = 1)] + Position = 1, + HelpMessage = "The tag to assign.")] [ValidateNotNullOrEmpty] public Tag? Tag { get; set; } @@ -26,14 +32,25 @@ public class AssignTagCmdlet : PSCmdlet { try { - var identifiers = new List { AddressObject!.Id.ToString() }; + var identifiers = new[] { AddressObject!.Id.ToString() }; var body = new Dictionary { - { "tag", Tag!.Id } + ["tag"] = Tag!.Id }; - RequestHelper.InvokeRequest("PATCH", controllers.addresses, null, null, body, identifiers.ToArray()) - .GetAwaiter().GetResult(); + 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) { diff --git a/classlib/Cmdlets/CloseSessionCmdlet.cs b/classlib/Cmdlets/CloseSessionCmdlet.cs index 07dabda..e5ee888 100644 --- a/classlib/Cmdlets/CloseSessionCmdlet.cs +++ b/classlib/Cmdlets/CloseSessionCmdlet.cs @@ -1,21 +1,17 @@ namespace PS.IPAM.Cmdlets; + using System.Management.Automation; using PS.IPAM.Helpers; +/// +/// Closes the current phpIPAM API session. +/// [Cmdlet(VerbsCommon.Close, "Session")] -public class CloseSessionCmdlet : PSCmdlet +public class CloseSessionCmdlet : BaseCmdlet { protected override void ProcessRecord() { - try - { - RequestHelper.InvokeRequest("DELETE", controllers.user, null, null, null, null) - .GetAwaiter().GetResult(); - SessionManager.CloseSession(); - } - catch (System.Exception ex) - { - WriteError(new ErrorRecord(ex, "CloseSessionError", ErrorCategory.InvalidOperation, null)); - } + SessionManager.CloseSession(); + WriteVerbose("Session closed successfully."); } } diff --git a/classlib/Cmdlets/GetAddressCmdlet.cs b/classlib/Cmdlets/GetAddressCmdlet.cs index b45d68d..9433c47 100644 --- a/classlib/Cmdlets/GetAddressCmdlet.cs +++ b/classlib/Cmdlets/GetAddressCmdlet.cs @@ -1,21 +1,25 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; using System.Net; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves IP address entries from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "Address", DefaultParameterSetName = "ByID")] [OutputType(typeof(Address))] -public class GetAddressCmdlet : PSCmdlet +public class GetAddressCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The address ID to retrieve.")] [ValidateNotNullOrEmpty] public int Id { get; set; } @@ -24,7 +28,8 @@ public class GetAddressCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByIP")] + ParameterSetName = "ByIP", + HelpMessage = "The IP address to search for.")] [Parameter( Mandatory = false, ValueFromPipeline = true, @@ -39,7 +44,8 @@ public class GetAddressCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByHostName")] + ParameterSetName = "ByHostName", + HelpMessage = "The hostname to search for.")] [ValidateNotNullOrEmpty] public string? HostName { get; set; } @@ -48,7 +54,8 @@ public class GetAddressCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByHostBase")] + ParameterSetName = "ByHostBase", + HelpMessage = "The hostname base pattern to search for.")] [ValidateNotNullOrEmpty] public string? HostBase { get; set; } @@ -57,7 +64,8 @@ public class GetAddressCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByTag")] + ParameterSetName = "ByTag", + HelpMessage = "The tag ID to filter addresses by.")] [ValidateNotNullOrEmpty] public int? TagId { get; set; } @@ -66,7 +74,8 @@ public class GetAddressCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySubnetId")] + ParameterSetName = "BySubnetId", + HelpMessage = "The subnet ID to get addresses from.")] [ValidateNotNullOrEmpty] public int? SubnetId { get; set; } @@ -75,7 +84,8 @@ public class GetAddressCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySubnetCIDR")] + 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; } @@ -84,7 +94,7 @@ public class GetAddressCmdlet : PSCmdlet { try { - var controller = controllers.addresses; + var controller = ApiController.Addresses; var identifiers = new List(); switch (ParameterSetName) @@ -92,23 +102,28 @@ public class GetAddressCmdlet : PSCmdlet 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) { @@ -117,42 +132,34 @@ public class GetAddressCmdlet : PSCmdlet } else { - controller = controllers.subnets; + controller = ApiController.Subnets; identifiers.Add(SubnetId!.Value.ToString()); identifiers.Add("addresses"); } break; + case "BySubnetCIDR": - controller = controllers.subnets; - var subnet = RequestHelper.InvokeRequest("GET", controllers.subnets, types.Subnetwork, null, null, new[] { "cidr", SubnetCIDR! }) - .GetAwaiter().GetResult(); + 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 Exception("Cannot find subnet!"); + throw new ItemNotFoundException($"Subnet '{SubnetCIDR}' not found."); } - var subnetObj = subnet as Subnetwork; - identifiers.Add(subnetObj!.Id.ToString()); + + identifiers.Add(subnet.Id.ToString()); identifiers.Add("addresses"); break; } - var result = RequestHelper.InvokeRequest("GET", controller, types.Address, null, null, identifiers.ToArray()) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "GET", controller, ModelType.Address, null, null, identifiers.ToArray() + ).GetAwaiter().GetResult(); - if (result != null) - { - if (result is System.Collections.IEnumerable enumerable && !(result is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(result); - } - } + WriteResult(result); } catch (Exception ex) { diff --git a/classlib/Cmdlets/GetFirstFreeIPCmdlet.cs b/classlib/Cmdlets/GetFirstFreeIPCmdlet.cs index ffd8ab8..e096077 100644 --- a/classlib/Cmdlets/GetFirstFreeIPCmdlet.cs +++ b/classlib/Cmdlets/GetFirstFreeIPCmdlet.cs @@ -1,20 +1,23 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using System.Net; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Gets the first available IP address in a subnet. +/// [Cmdlet(VerbsCommon.Get, "FirstFreeIP", DefaultParameterSetName = "ByID")] -public class GetFirstFreeIPCmdlet : PSCmdlet +public class GetFirstFreeIPCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByCIDR")] + ParameterSetName = "ByCIDR", + HelpMessage = "The subnet in CIDR notation.")] [ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")] [ValidateNotNullOrEmpty] public string? CIDR { get; set; } @@ -24,7 +27,8 @@ public class GetFirstFreeIPCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The subnet ID.")] [ValidateNotNullOrEmpty] public int? Id { get; set; } @@ -33,7 +37,8 @@ public class GetFirstFreeIPCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySubnetObject")] + ParameterSetName = "BySubnetObject", + HelpMessage = "The subnet object.")] [ValidateNotNullOrEmpty] public Subnetwork? SubnetObject { get; set; } @@ -41,30 +46,12 @@ public class GetFirstFreeIPCmdlet : PSCmdlet { try { - int subnetId; - if (ParameterSetName == "ByCIDR") - { - var subnet = RequestHelper.InvokeRequest("GET", controllers.subnets, types.Subnetwork, null, null, new[] { "cidr", CIDR! }) - .GetAwaiter().GetResult(); - if (subnet == null) - { - throw new Exception("Cannot find subnet!"); - } - var subnetObj = subnet as Subnetwork; - subnetId = subnetObj!.Id; - } - else if (ParameterSetName == "BySubnetObject") - { - subnetId = SubnetObject!.Id; - } - else - { - subnetId = Id!.Value; - } - + var subnetId = GetSubnetId(); var identifiers = new List { subnetId.ToString(), "first_free" }; - var result = RequestHelper.InvokeRequest("GET", controllers.subnets, null, null, null, identifiers.ToArray()) - .GetAwaiter().GetResult(); + + var result = RequestHelper.InvokeRequest( + "GET", ApiController.Subnets, null, null, null, identifiers.ToArray() + ).GetAwaiter().GetResult(); if (result != null) { @@ -76,4 +63,28 @@ public class GetFirstFreeIPCmdlet : PSCmdlet 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; + } + } } diff --git a/classlib/Cmdlets/GetL2DomainCmdlet.cs b/classlib/Cmdlets/GetL2DomainCmdlet.cs index 4839d56..e66918d 100644 --- a/classlib/Cmdlets/GetL2DomainCmdlet.cs +++ b/classlib/Cmdlets/GetL2DomainCmdlet.cs @@ -1,20 +1,24 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves L2 domain information from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "L2Domain", DefaultParameterSetName = "ByID")] [OutputType(typeof(Domain))] -public class GetL2DomainCmdlet : PSCmdlet +public class GetL2DomainCmdlet : BaseCmdlet { [Parameter( Mandatory = false, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The L2 domain ID.")] [ValidateNotNullOrEmpty] public int? Id { get; set; } @@ -23,28 +27,18 @@ public class GetL2DomainCmdlet : PSCmdlet try { var identifiers = new List(); + if (Id.HasValue) { identifiers.Add(Id.Value.ToString()); } - var result = RequestHelper.InvokeRequest("GET", controllers.l2domains, types.Domain, null, null, identifiers.Count > 0 ? identifiers.ToArray() : null) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "GET", ApiController.L2Domains, ModelType.Domain, null, null, + identifiers.Count > 0 ? identifiers.ToArray() : null + ).GetAwaiter().GetResult(); - if (result != null) - { - if (result is System.Collections.IEnumerable enumerable && !(result is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(result); - } - } + WriteResult(result); } catch (Exception ex) { diff --git a/classlib/Cmdlets/GetNameserverCmdlet.cs b/classlib/Cmdlets/GetNameserverCmdlet.cs index 2bb91f5..7533038 100644 --- a/classlib/Cmdlets/GetNameserverCmdlet.cs +++ b/classlib/Cmdlets/GetNameserverCmdlet.cs @@ -1,20 +1,24 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves nameserver information from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "Nameserver", DefaultParameterSetName = "NoParams")] [OutputType(typeof(Nameserver))] -public class GetNameserverCmdlet : PSCmdlet +public class GetNameserverCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The nameserver ID.")] [ValidateNotNullOrEmpty] public int? Id { get; set; } @@ -23,28 +27,18 @@ public class GetNameserverCmdlet : PSCmdlet try { var identifiers = new List(); + if (Id.HasValue) { identifiers.Add(Id.Value.ToString()); } - var result = RequestHelper.InvokeRequest("GET", controllers.tools, types.Nameserver, subcontrollers.nameservers, null, identifiers.Count > 0 ? identifiers.ToArray() : null) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "GET", ApiController.Tools, ModelType.Nameserver, ApiSubController.Nameservers, + null, identifiers.Count > 0 ? identifiers.ToArray() : null + ).GetAwaiter().GetResult(); - if (result != null) - { - if (result is System.Collections.IEnumerable enumerable && !(result is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(result); - } - } + WriteResult(result); } catch (Exception ex) { diff --git a/classlib/Cmdlets/GetPermissionsCmdlet.cs b/classlib/Cmdlets/GetPermissionsCmdlet.cs index 4c476fb..de8284f 100644 --- a/classlib/Cmdlets/GetPermissionsCmdlet.cs +++ b/classlib/Cmdlets/GetPermissionsCmdlet.cs @@ -1,20 +1,24 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; using System.Net; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves permission information from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "Permissions")] -public class GetPermissionsCmdlet : PSCmdlet +public class GetPermissionsCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The address ID.")] [ValidateNotNullOrEmpty] public string? Id { get; set; } @@ -23,7 +27,8 @@ public class GetPermissionsCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByIP")] + ParameterSetName = "ByIP", + HelpMessage = "The IP address to search for.")] [Parameter( Mandatory = false, ValueFromPipeline = true, @@ -38,7 +43,8 @@ public class GetPermissionsCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByHostName")] + ParameterSetName = "ByHostName", + HelpMessage = "The hostname to search for.")] [ValidateNotNullOrEmpty] public string? HostName { get; set; } @@ -47,7 +53,8 @@ public class GetPermissionsCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByTag")] + ParameterSetName = "ByTag", + HelpMessage = "The tag ID.")] [ValidateNotNullOrEmpty] public string? TagId { get; set; } @@ -56,7 +63,8 @@ public class GetPermissionsCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySubnetId")] + ParameterSetName = "BySubnetId", + HelpMessage = "The subnet ID.")] [ValidateNotNullOrEmpty] public string? SubnetId { get; set; } @@ -65,7 +73,8 @@ public class GetPermissionsCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySubnetCIDR")] + ParameterSetName = "BySubnetCIDR", + HelpMessage = "The subnet in CIDR notation.")] [ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")] [ValidateNotNullOrEmpty] public string? SubnetCIDR { get; set; } @@ -74,7 +83,7 @@ public class GetPermissionsCmdlet : PSCmdlet { try { - var controller = controllers.addresses; + var controller = ApiController.Addresses; var identifiers = new List(); switch (ParameterSetName) @@ -82,19 +91,23 @@ public class GetPermissionsCmdlet : PSCmdlet 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) { @@ -103,42 +116,34 @@ public class GetPermissionsCmdlet : PSCmdlet } else { - controller = controllers.subnets; + controller = ApiController.Subnets; identifiers.Add(SubnetId!); identifiers.Add("addresses"); } break; + case "BySubnetCIDR": - controller = controllers.subnets; - var subnet = RequestHelper.InvokeRequest("GET", controllers.subnets, types.Subnetwork, null, null, new[] { "cidr", SubnetCIDR! }) - .GetAwaiter().GetResult(); + 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 Exception("Cannot find subnet!"); + throw new ItemNotFoundException($"Subnet '{SubnetCIDR}' not found."); } - var subnetObj = subnet as Subnetwork; - identifiers.Add(subnetObj!.Id.ToString()); + + identifiers.Add(subnet.Id.ToString()); identifiers.Add("addresses"); break; } - var result = RequestHelper.InvokeRequest("GET", controller, null, null, null, identifiers.ToArray()) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "GET", controller, null, null, null, identifiers.ToArray() + ).GetAwaiter().GetResult(); - if (result != null) - { - if (result is System.Collections.IEnumerable enumerable && !(result is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(result); - } - } + WriteResult(result); } catch (Exception ex) { diff --git a/classlib/Cmdlets/GetSectionCmdlet.cs b/classlib/Cmdlets/GetSectionCmdlet.cs index 97dd9e2..50fc250 100644 --- a/classlib/Cmdlets/GetSectionCmdlet.cs +++ b/classlib/Cmdlets/GetSectionCmdlet.cs @@ -1,19 +1,23 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves section information from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "Section", DefaultParameterSetName = "NoParams")] [OutputType(typeof(Section))] -public class GetSectionCmdlet : PSCmdlet +public class GetSectionCmdlet : BaseCmdlet { [Parameter( Mandatory = false, ValueFromPipeline = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The section ID.")] [ValidateNotNullOrEmpty] public int? Id { get; set; } @@ -21,7 +25,8 @@ public class GetSectionCmdlet : PSCmdlet Mandatory = true, ValueFromPipeline = true, Position = 0, - ParameterSetName = "ByName")] + ParameterSetName = "ByName", + HelpMessage = "The section name.")] [ValidateNotNullOrEmpty] public string? Name { get; set; } @@ -30,6 +35,7 @@ public class GetSectionCmdlet : PSCmdlet try { var identifiers = new List(); + if (ParameterSetName == "ByID" && Id.HasValue) { identifiers.Add(Id.Value.ToString()); @@ -39,23 +45,12 @@ public class GetSectionCmdlet : PSCmdlet identifiers.Add(Name!); } - var result = RequestHelper.InvokeRequest("GET", controllers.sections, types.Section, null, null, identifiers.Count > 0 ? identifiers.ToArray() : null) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "GET", ApiController.Sections, ModelType.Section, null, null, + identifiers.Count > 0 ? identifiers.ToArray() : null + ).GetAwaiter().GetResult(); - if (result != null) - { - if (result is System.Collections.IEnumerable enumerable && !(result is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(result); - } - } + WriteResult(result); } catch (Exception ex) { diff --git a/classlib/Cmdlets/GetSubnetCmdlet.cs b/classlib/Cmdlets/GetSubnetCmdlet.cs index d2c3f52..0f08d3e 100644 --- a/classlib/Cmdlets/GetSubnetCmdlet.cs +++ b/classlib/Cmdlets/GetSubnetCmdlet.cs @@ -1,22 +1,24 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; -using System.Linq; using System.Management.Automation; -using System.Net; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves subnet information from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "Subnet", DefaultParameterSetName = "NoParams")] [OutputType(typeof(Subnetwork))] -public class GetSubnetCmdlet : PSCmdlet +public class GetSubnetCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByCIDR")] + 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; } @@ -26,7 +28,8 @@ public class GetSubnetCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The subnet ID.")] [ValidateNotNullOrEmpty] public int? Id { get; set; } @@ -35,7 +38,8 @@ public class GetSubnetCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySectionId")] + ParameterSetName = "BySectionId", + HelpMessage = "The section ID to get subnets from.")] [Parameter( Mandatory = false, ValueFromPipeline = true, @@ -56,7 +60,8 @@ public class GetSubnetCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySectionName")] + ParameterSetName = "BySectionName", + HelpMessage = "The section name to get subnets from.")] [Parameter( Mandatory = false, ValueFromPipeline = true, @@ -77,7 +82,8 @@ public class GetSubnetCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByVrfId")] + ParameterSetName = "ByVrfId", + HelpMessage = "The VRF ID to get subnets from.")] [ValidateNotNullOrEmpty] public int? VrfId { get; set; } @@ -86,7 +92,8 @@ public class GetSubnetCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByVlanId")] + ParameterSetName = "ByVlanId", + HelpMessage = "The VLAN ID to get subnets from.")] [ValidateNotNullOrEmpty] public int? VlanId { get; set; } @@ -95,7 +102,8 @@ public class GetSubnetCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByVlanNumber")] + ParameterSetName = "ByVlanNumber", + HelpMessage = "The VLAN number to get subnets from.")] [ValidateNotNullOrEmpty] public int? VlanNumber { get; set; } @@ -104,7 +112,8 @@ public class GetSubnetCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 1, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "Get child subnets (slaves).")] public SwitchParameter Slaves { get; set; } [Parameter( @@ -112,7 +121,8 @@ public class GetSubnetCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 2, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "Get child subnets recursively.")] public SwitchParameter Recurse { get; set; } [Parameter( @@ -120,16 +130,16 @@ public class GetSubnetCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 1, - ParameterSetName = "ByVlanNumber")] + ParameterSetName = "ByVlanNumber", + HelpMessage = "The L2 domain ID to narrow VLAN search.")] [ValidateNotNullOrEmpty] public int? VlanDomainId { get; set; } - protected override void ProcessRecord() { try { - var controller = controllers.subnets; + var controller = ApiController.Subnets; var identifiers = new List(); switch (ParameterSetName) @@ -138,6 +148,7 @@ public class GetSubnetCmdlet : PSCmdlet identifiers.Add("cidr"); identifiers.Add(CIDR!); break; + case "ByID": identifiers.Add(Id!.Value.ToString()); if (Slaves.IsPresent) @@ -145,117 +156,122 @@ public class GetSubnetCmdlet : PSCmdlet identifiers.Add(Recurse.IsPresent ? "slaves_recursive" : "slaves"); } break; + case "BySectionId": - controller = controllers.sections; + controller = ApiController.Sections; identifiers.Add(SectionId!.Value.ToString()); identifiers.Add("subnets"); break; + case "BySectionName": - controller = controllers.sections; - var section = RequestHelper.InvokeRequest("GET", controllers.sections, types.Section, null, null, new[] { SectionName! }) - .GetAwaiter().GetResult(); - if (section == null) - { - throw new Exception("Cannot find section!"); - } - var sectionObj = section as Section; - identifiers.Add(sectionObj!.Id.ToString()); + controller = ApiController.Sections; + var section = GetSectionByName(SectionName!); + identifiers.Add(section.Id.ToString()); identifiers.Add("subnets"); break; + case "ByVrfId": - controller = controllers.vrf; + controller = ApiController.Vrf; identifiers.Add(VrfId!.Value.ToString()); identifiers.Add("subnets"); break; + case "ByVlanId": - controller = controllers.vlan; + controller = ApiController.Vlan; identifiers.Add(VlanId!.Value.ToString()); identifiers.Add("subnets"); - if (SectionId.HasValue) - { - identifiers.Add(SectionId.Value.ToString()); - } - else if (!string.IsNullOrEmpty(SectionName)) - { - var section2 = RequestHelper.InvokeRequest("GET", controllers.sections, types.Section, null, null, new[] { SectionName }) - .GetAwaiter().GetResult(); - if (section2 != null) - { - var sectionObj2 = section2 as Section; - identifiers.Add(sectionObj2!.Id.ToString()); - } - } + AddSectionIdentifier(identifiers); break; + case "ByVlanNumber": - controller = controllers.vlan; - var vlans = RequestHelper.InvokeRequest("GET", controllers.vlan, types.Vlan, null, null, new[] { "search", VlanNumber!.Value.ToString() }) - .GetAwaiter().GetResult(); - if (vlans == null) - { - throw new Exception("Cannot find Vlan!"); - } - var vlanList = vlans as System.Collections.IEnumerable; - Vlan? foundVlan = null; - if (vlanList != null) - { - foreach (var v in vlanList) - { - if (v is Vlan vlan) - { - if (VlanDomainId.HasValue && vlan.DomainId != VlanDomainId.Value) - continue; - if (foundVlan != null) - { - throw new Exception($"More than one vLan with {VlanNumber} number is present!"); - } - foundVlan = vlan; - } - } - } - if (foundVlan == null) - { - throw new Exception("Cannot find Vlan!"); - } - identifiers.Add(foundVlan.Id.ToString()); + controller = ApiController.Vlan; + var vlan = FindVlanByNumber(VlanNumber!.Value, VlanDomainId); + identifiers.Add(vlan.Id.ToString()); identifiers.Add("subnets"); - if (SectionId.HasValue) - { - identifiers.Add(SectionId.Value.ToString()); - } - else if (!string.IsNullOrEmpty(SectionName)) - { - var section3 = RequestHelper.InvokeRequest("GET", controllers.sections, types.Section, null, null, new[] { SectionName }) - .GetAwaiter().GetResult(); - if (section3 != null) - { - var sectionObj3 = section3 as Section; - identifiers.Add(sectionObj3!.Id.ToString()); - } - } + AddSectionIdentifier(identifiers); break; } - var result = RequestHelper.InvokeRequest("GET", controller, types.Subnetwork, null, null, identifiers.ToArray()) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "GET", controller, ModelType.Subnetwork, null, null, identifiers.ToArray() + ).GetAwaiter().GetResult(); - if (result != null) - { - if (result is System.Collections.IEnumerable enumerable && !(result is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(result); - } - } + 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 identifiers) + { + if (SectionId.HasValue) + { + identifiers.Add(SectionId.Value.ToString()); + } + else if (!string.IsNullOrEmpty(SectionName)) + { + var section = GetSectionByName(SectionName); + identifiers.Add(section.Id.ToString()); + } + } } diff --git a/classlib/Cmdlets/GetSubnetUsageCmdlet.cs b/classlib/Cmdlets/GetSubnetUsageCmdlet.cs index d4bd301..bedc68b 100644 --- a/classlib/Cmdlets/GetSubnetUsageCmdlet.cs +++ b/classlib/Cmdlets/GetSubnetUsageCmdlet.cs @@ -1,20 +1,23 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using System.Net; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves subnet usage statistics from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "SubnetUsage", DefaultParameterSetName = "ByID")] -public class GetSubnetUsageCmdlet : PSCmdlet +public class GetSubnetUsageCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByCIDR")] + ParameterSetName = "ByCIDR", + HelpMessage = "The subnet in CIDR notation.")] [ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")] [ValidateNotNullOrEmpty] public string? CIDR { get; set; } @@ -24,43 +27,60 @@ public class GetSubnetUsageCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + 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 { - int subnetId; - if (ParameterSetName == "ByCIDR") - { - var subnet = RequestHelper.InvokeRequest("GET", controllers.subnets, types.Subnetwork, null, null, new[] { "cidr", CIDR! }) - .GetAwaiter().GetResult(); - if (subnet == null) - { - throw new Exception("Cannot find subnet!"); - } - var subnetObj = subnet as Subnetwork; - subnetId = subnetObj!.Id; - } - else - { - subnetId = Id!.Value; - } - + var subnetId = GetSubnetId(); var identifiers = new List { subnetId.ToString(), "usage" }; - var result = RequestHelper.InvokeRequest("GET", controllers.subnets, null, null, null, identifiers.ToArray()) - .GetAwaiter().GetResult(); - if (result != null) - { - WriteObject(result); - } + 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; + } + } } diff --git a/classlib/Cmdlets/GetTagCmdlet.cs b/classlib/Cmdlets/GetTagCmdlet.cs index 1c0d3a7..cc9d1cd 100644 --- a/classlib/Cmdlets/GetTagCmdlet.cs +++ b/classlib/Cmdlets/GetTagCmdlet.cs @@ -1,20 +1,24 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves tag information from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "Tag", DefaultParameterSetName = "NoParams")] [OutputType(typeof(Tag))] -public class GetTagCmdlet : PSCmdlet +public class GetTagCmdlet : BaseCmdlet { [Parameter( Mandatory = false, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The tag ID.")] [ValidateNotNullOrEmpty] public int? Id { get; set; } @@ -23,7 +27,8 @@ public class GetTagCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByAddressObject")] + ParameterSetName = "ByAddressObject", + HelpMessage = "Get the tag associated with this address.")] [ValidateNotNullOrEmpty] public Address? AddressObject { get; set; } @@ -32,7 +37,8 @@ public class GetTagCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySubnetObject")] + ParameterSetName = "BySubnetObject", + HelpMessage = "Get the tag associated with this subnet.")] [ValidateNotNullOrEmpty] public Subnetwork? SubnetObject { get; set; } @@ -50,6 +56,7 @@ public class GetTagCmdlet : PSCmdlet identifiers.Add(Id.Value.ToString()); } break; + case "ByAddressObject": if (AddressObject?.TagId > 0) { @@ -60,6 +67,7 @@ public class GetTagCmdlet : PSCmdlet return; } break; + case "BySubnetObject": if (SubnetObject?.TagId > 0) { @@ -72,23 +80,11 @@ public class GetTagCmdlet : PSCmdlet break; } - var result = RequestHelper.InvokeRequest("GET", controllers.addresses, types.Tag, null, null, identifiers.ToArray()) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "GET", ApiController.Addresses, ModelType.Tag, null, null, identifiers.ToArray() + ).GetAwaiter().GetResult(); - if (result != null) - { - if (result is System.Collections.IEnumerable enumerable && !(result is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(result); - } - } + WriteResult(result); } catch (Exception ex) { diff --git a/classlib/Cmdlets/GetVlanCmdlet.cs b/classlib/Cmdlets/GetVlanCmdlet.cs index 7425b19..06d9c82 100644 --- a/classlib/Cmdlets/GetVlanCmdlet.cs +++ b/classlib/Cmdlets/GetVlanCmdlet.cs @@ -1,20 +1,24 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves VLAN information from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "Vlan", DefaultParameterSetName = "NoParams")] [OutputType(typeof(Vlan))] -public class GetVlanCmdlet : PSCmdlet +public class GetVlanCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The VLAN ID.")] [ValidateNotNullOrEmpty] public int? Id { get; set; } @@ -23,7 +27,8 @@ public class GetVlanCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByNumber")] + ParameterSetName = "ByNumber", + HelpMessage = "The VLAN number.")] [ValidateNotNullOrEmpty] public int? Number { get; set; } @@ -32,7 +37,8 @@ public class GetVlanCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByL2Domain")] + ParameterSetName = "ByL2Domain", + HelpMessage = "The L2 domain ID to get VLANs from.")] [ValidateNotNullOrEmpty] public int? L2DomainId { get; set; } @@ -41,7 +47,8 @@ public class GetVlanCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySubnetObject")] + ParameterSetName = "BySubnetObject", + HelpMessage = "Get the VLAN associated with this subnet.")] [ValidateNotNullOrEmpty] public Subnetwork? SubnetObject { get; set; } @@ -50,7 +57,8 @@ public class GetVlanCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByDomainObject")] + ParameterSetName = "ByDomainObject", + HelpMessage = "Get VLANs in this L2 domain.")] [ValidateNotNullOrEmpty] public Domain? DomainObject { get; set; } @@ -58,7 +66,7 @@ public class GetVlanCmdlet : PSCmdlet { try { - var controller = controllers.vlan; + var controller = ApiController.Vlan; var identifiers = new List(); switch (ParameterSetName) @@ -66,65 +74,44 @@ public class GetVlanCmdlet : PSCmdlet case "ByID": identifiers.Add(Id!.Value.ToString()); break; + case "ByNumber": identifiers.Add("search"); identifiers.Add(Number!.Value.ToString()); break; + case "ByL2Domain": - controller = controllers.l2domains; + controller = ApiController.L2Domains; identifiers.Add(L2DomainId!.Value.ToString()); - identifiers.Add(subcontrollers.vlans.ToString()); + identifiers.Add(ApiSubController.Vlans.ToString().ToLowerInvariant()); break; + case "BySubnetObject": - if (SubnetObject != null && SubnetObject.VlanId > 0) - { - identifiers.Add(SubnetObject.VlanId.ToString()); - } - else + if (SubnetObject == null || SubnetObject.VlanId <= 0) { return; } + identifiers.Add(SubnetObject.VlanId.ToString()); break; + case "ByDomainObject": if (DomainObject != null) { - var result = RequestHelper.InvokeRequest("GET", controllers.l2domains, types.Vlan, subcontrollers.vlans, null, new[] { DomainObject.Id.ToString() }) - .GetAwaiter().GetResult(); - if (result != null) - { - if (result is System.Collections.IEnumerable enumerable && !(result is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(result); - } - } + 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, types.Vlan, null, null, identifiers.Count > 0 ? identifiers.ToArray() : null) - .GetAwaiter().GetResult(); + var vlanResult = RequestHelper.InvokeRequest( + "GET", controller, ModelType.Vlan, null, null, + identifiers.Count > 0 ? identifiers.ToArray() : null + ).GetAwaiter().GetResult(); - if (vlanResult != null) - { - if (vlanResult is System.Collections.IEnumerable enumerable && !(vlanResult is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(vlanResult); - } - } + WriteResult(vlanResult); } catch (Exception ex) { diff --git a/classlib/Cmdlets/GetVrfCmdlet.cs b/classlib/Cmdlets/GetVrfCmdlet.cs index 898cd80..755f25c 100644 --- a/classlib/Cmdlets/GetVrfCmdlet.cs +++ b/classlib/Cmdlets/GetVrfCmdlet.cs @@ -1,20 +1,24 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Retrieves VRF information from phpIPAM. +/// [Cmdlet(VerbsCommon.Get, "Vrf", DefaultParameterSetName = "NoParams")] [OutputType(typeof(Vrf))] -public class GetVrfCmdlet : PSCmdlet +public class GetVrfCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByID")] + ParameterSetName = "ByID", + HelpMessage = "The VRF ID.")] [ValidateNotNullOrEmpty] public int? Id { get; set; } @@ -23,28 +27,18 @@ public class GetVrfCmdlet : PSCmdlet try { var identifiers = new List(); + if (Id.HasValue) { identifiers.Add(Id.Value.ToString()); } - var result = RequestHelper.InvokeRequest("GET", controllers.vrf, types.Vrf, null, null, identifiers.Count > 0 ? identifiers.ToArray() : null) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "GET", ApiController.Vrf, ModelType.Vrf, null, null, + identifiers.Count > 0 ? identifiers.ToArray() : null + ).GetAwaiter().GetResult(); - if (result != null) - { - if (result is System.Collections.IEnumerable enumerable && !(result is string)) - { - foreach (var item in enumerable) - { - WriteObject(item); - } - } - else - { - WriteObject(result); - } - } + WriteResult(result); } catch (Exception ex) { diff --git a/classlib/Cmdlets/NewAddressCmdlet.cs b/classlib/Cmdlets/NewAddressCmdlet.cs index def3663..66ce858 100644 --- a/classlib/Cmdlets/NewAddressCmdlet.cs +++ b/classlib/Cmdlets/NewAddressCmdlet.cs @@ -1,21 +1,24 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using System.Net; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Creates a new IP address entry in phpIPAM. +/// [Cmdlet(VerbsCommon.New, "Address", DefaultParameterSetName = "BySubnetId")] [OutputType(typeof(Address))] -public class NewAddressCmdlet : PSCmdlet +public class NewAddressCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySubnetId")] + ParameterSetName = "BySubnetId", + HelpMessage = "The subnet ID to create the address in.")] [ValidateNotNullOrEmpty] public int? SubnetId { get; set; } @@ -24,7 +27,8 @@ public class NewAddressCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "BySubnetObject")] + ParameterSetName = "BySubnetObject", + HelpMessage = "The subnet object to create the address in.")] [ValidateNotNullOrEmpty] public Subnetwork? SubnetObject { get; set; } @@ -32,166 +36,73 @@ public class NewAddressCmdlet : PSCmdlet Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, - Position = 1)] + Position = 1, + HelpMessage = "The IP address to create.")] [ValidateNotNullOrEmpty] public string IP { get; set; } = string.Empty; - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 2)] + [Parameter(Mandatory = false, Position = 2, HelpMessage = "Mark this address as a gateway.")] public SwitchParameter Gateway { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 3)] + [Parameter(Mandatory = false, Position = 3, HelpMessage = "Description for the address.")] public string? Description { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 4)] + [Parameter(Mandatory = false, Position = 4, HelpMessage = "Hostname for the address.")] public string? Hostname { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 5)] + [Parameter(Mandatory = false, Position = 5, HelpMessage = "MAC address.")] public string? MAC { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 6)] + [Parameter(Mandatory = false, Position = 6, HelpMessage = "Owner of the address.")] public string? Owner { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 7)] + [Parameter(Mandatory = false, Position = 7, HelpMessage = "Tag ID for the address.")] public int? TagId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 8)] + [Parameter(Mandatory = false, Position = 8, HelpMessage = "Ignore PTR record generation.")] public SwitchParameter PTRIgnore { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 7)] + [Parameter(Mandatory = false, Position = 9, HelpMessage = "PTR record ID.")] public int? PTRId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 10)] + [Parameter(Mandatory = false, Position = 10, HelpMessage = "Note for the address.")] public string? Note { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 11)] + [Parameter(Mandatory = false, Position = 11, HelpMessage = "Exclude from ping scanning.")] public SwitchParameter ExcludePing { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 12)] + [Parameter(Mandatory = false, Position = 12, HelpMessage = "Associated device ID.")] public int? DeviceId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 13)] + [Parameter(Mandatory = false, Position = 13, HelpMessage = "Port information.")] public string? Port { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 14)] + [Parameter(Mandatory = false, Position = 14, HelpMessage = "Custom fields as a hashtable or PSObject.")] public object? CustomFields { get; set; } protected override void ProcessRecord() { try { - int actualSubnetId; - if (ParameterSetName == "BySubnetObject") - { - actualSubnetId = SubnetObject!.Id; - } - else - { - actualSubnetId = SubnetId!.Value; - } + var actualSubnetId = ParameterSetName == "BySubnetObject" + ? SubnetObject!.Id + : SubnetId!.Value; - var body = new Dictionary - { - { "subnetId", actualSubnetId }, - { "ip", IP } - }; + var body = BuildRequestBody(actualSubnetId); - 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; - - if (CustomFields != null) - { - var customDict = ConvertCustomFields(CustomFields); - foreach (var kvp in customDict) - { - body[kvp.Key] = kvp.Value; - } - } - - var result = RequestHelper.InvokeRequest("POST", controllers.addresses, null, null, body, null) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "POST", ApiController.Addresses, null, null, body + ).GetAwaiter().GetResult(); if (result != null) { - var address = RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "search", IP }) - .GetAwaiter().GetResult(); - if (address != null) - { - WriteObject(address); - } + // 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) @@ -200,20 +111,32 @@ public class NewAddressCmdlet : PSCmdlet } } - private Dictionary ConvertCustomFields(object customFields) + private Dictionary BuildRequestBody(int subnetId) { - var dict = new Dictionary(); - if (customFields is PSObject psobj) + var body = new Dictionary { - foreach (var prop in psobj.Properties) - { - dict[prop.Name] = prop.Value ?? new object(); - } - } - else if (customFields is Dictionary dictObj) + ["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)) { - return dictObj; + body[kvp.Key] = kvp.Value; } - return dict; + + return body; } } diff --git a/classlib/Cmdlets/NewFirstFreeIPCmdlet.cs b/classlib/Cmdlets/NewFirstFreeIPCmdlet.cs index ca48fff..cecbd66 100644 --- a/classlib/Cmdlets/NewFirstFreeIPCmdlet.cs +++ b/classlib/Cmdlets/NewFirstFreeIPCmdlet.cs @@ -1,168 +1,85 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Creates a new address using the first available IP in a subnet. +/// [Cmdlet(VerbsCommon.New, "FirstFreeIP")] [OutputType(typeof(Address))] -public class NewFirstFreeIPCmdlet : PSCmdlet +public class NewFirstFreeIPCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, - Position = 0)] + Position = 0, + HelpMessage = "The subnet ID to create the address in.")] [ValidateNotNullOrEmpty] public string SubnetId { get; set; } = string.Empty; - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 2)] + [Parameter(Mandatory = false, Position = 1, HelpMessage = "Mark this address as a gateway.")] public SwitchParameter Gateway { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 3)] + [Parameter(Mandatory = false, Position = 2, HelpMessage = "Description for the address.")] public string? Description { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 4)] + [Parameter(Mandatory = false, Position = 3, HelpMessage = "Hostname for the address.")] public string? Hostname { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 5)] + [Parameter(Mandatory = false, Position = 4, HelpMessage = "MAC address.")] public string? MAC { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 6)] + [Parameter(Mandatory = false, Position = 5, HelpMessage = "Owner of the address.")] public string? Owner { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 7)] + [Parameter(Mandatory = false, Position = 6, HelpMessage = "Tag ID for the address.")] public string? TagId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 8)] + [Parameter(Mandatory = false, Position = 7, HelpMessage = "Ignore PTR record generation.")] public SwitchParameter PTRIgnore { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 9)] + [Parameter(Mandatory = false, Position = 8, HelpMessage = "PTR record ID.")] public string? PTRId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 10)] + [Parameter(Mandatory = false, Position = 9, HelpMessage = "Note for the address.")] public string? Note { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 11)] + [Parameter(Mandatory = false, Position = 10, HelpMessage = "Exclude from ping scanning.")] public SwitchParameter ExcludePing { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 12)] + [Parameter(Mandatory = false, Position = 11, HelpMessage = "Associated device ID.")] public string? DeviceId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 13)] + [Parameter(Mandatory = false, Position = 12, HelpMessage = "Port information.")] public string? Port { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true)] + [Parameter(Mandatory = false, HelpMessage = "Custom fields as a hashtable or PSObject.")] public object? CustomFields { get; set; } protected override void ProcessRecord() { try { - var identifiers = new List { "first_free" }; - var body = new Dictionary - { - { "subnetId", SubnetId } - }; + var identifiers = new[] { "first_free" }; + var body = BuildRequestBody(); - 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; - - if (CustomFields != null) - { - var customDict = ConvertCustomFields(CustomFields); - foreach (var kvp in customDict) - { - body[kvp.Key] = kvp.Value; - } - } - - var result = RequestHelper.InvokeRequest("POST", controllers.addresses, null, null, body, identifiers.ToArray()) - .GetAwaiter().GetResult(); + 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", controllers.addresses, types.Address, null, null, new[] { "search", ip }) - .GetAwaiter().GetResult(); - if (address != null) - { - WriteObject(address); - } + var address = RequestHelper.InvokeRequest( + "GET", ApiController.Addresses, ModelType.Address, null, null, + new[] { "search", ip! } + ).GetAwaiter().GetResult(); + + WriteResult(address); } } catch (Exception ex) @@ -171,20 +88,31 @@ public class NewFirstFreeIPCmdlet : PSCmdlet } } - private Dictionary ConvertCustomFields(object customFields) + private Dictionary BuildRequestBody() { - var dict = new Dictionary(); - if (customFields is PSObject psobj) + var body = new Dictionary { - foreach (var prop in psobj.Properties) - { - dict[prop.Name] = prop.Value ?? new object(); - } - } - else if (customFields is Dictionary dictObj) + ["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)) { - return dictObj; + body[kvp.Key] = kvp.Value; } - return dict; + + return body; } } diff --git a/classlib/Cmdlets/NewSessionCmdlet.cs b/classlib/Cmdlets/NewSessionCmdlet.cs index 2f89eb4..3fe98ba 100644 --- a/classlib/Cmdlets/NewSessionCmdlet.cs +++ b/classlib/Cmdlets/NewSessionCmdlet.cs @@ -1,18 +1,22 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Management.Automation; -using System.Threading.Tasks; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Creates a new phpIPAM API session. +/// [Cmdlet(VerbsCommon.New, "Session", DefaultParameterSetName = "Credentials")] -public class NewSessionCmdlet : PSCmdlet +[OutputType(typeof(Session))] +public class NewSessionCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, - Position = 0)] + Position = 0, + HelpMessage = "The phpIPAM server URL (must start with http:// or https://).")] [ValidateNotNullOrEmpty] [ValidatePattern("^https?://")] public string URL { get; set; } = string.Empty; @@ -21,7 +25,8 @@ public class NewSessionCmdlet : PSCmdlet Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, - Position = 1)] + Position = 1, + HelpMessage = "The API application ID configured in phpIPAM.")] [ValidateNotNullOrEmpty] public string AppID { get; set; } = string.Empty; @@ -30,7 +35,8 @@ public class NewSessionCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 2, - ParameterSetName = "Credentials")] + ParameterSetName = "Credentials", + HelpMessage = "The credentials (username and password) for authentication.")] [ValidateNotNullOrEmpty] public PSCredential? Credentials { get; set; } @@ -39,7 +45,8 @@ public class NewSessionCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 2, - ParameterSetName = "Token")] + ParameterSetName = "Token", + HelpMessage = "The static API token for authentication.")] [ValidateNotNullOrEmpty] public string? Token { get; set; } @@ -47,7 +54,8 @@ public class NewSessionCmdlet : PSCmdlet Mandatory = false, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, - Position = 3)] + Position = 3, + HelpMessage = "If specified, SSL certificate errors will be ignored.")] public SwitchParameter IgnoreSSL { get; set; } protected override void ProcessRecord() @@ -55,9 +63,10 @@ public class NewSessionCmdlet : PSCmdlet try { Session session; + if (ParameterSetName == "Credentials" && Credentials != null) { - session = SessionManager.CreateSessionWithCredentials( + session = SessionManager.CreateSessionWithCredentialsAsync( URL, AppID, Credentials, @@ -70,12 +79,14 @@ public class NewSessionCmdlet : PSCmdlet } else { - throw new ArgumentException("Invalid parameter set"); + throw new ArgumentException("Invalid parameter set. Provide either Credentials or Token."); } + + WriteObject(session); } catch (Exception ex) { - WriteError(new ErrorRecord(ex, "NewSessionError", ErrorCategory.InvalidOperation, null)); + WriteError(new ErrorRecord(ex, "NewSessionError", ErrorCategory.AuthenticationError, null)); } } } diff --git a/classlib/Cmdlets/NewSubnetCmdlet.cs b/classlib/Cmdlets/NewSubnetCmdlet.cs index cf2d41b..a34f5c5 100644 --- a/classlib/Cmdlets/NewSubnetCmdlet.cs +++ b/classlib/Cmdlets/NewSubnetCmdlet.cs @@ -1,20 +1,23 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using System.Net; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Creates a new subnet in phpIPAM. +/// [Cmdlet(VerbsCommon.New, "Subnet")] [OutputType(typeof(Subnetwork))] -public class NewSubnetCmdlet : PSCmdlet +public class NewSubnetCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, - Position = 0)] + 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; @@ -23,187 +26,79 @@ public class NewSubnetCmdlet : PSCmdlet Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, - Position = 1)] + Position = 1, + HelpMessage = "The section ID to create the subnet in.")] [ValidateNotNullOrEmpty] public int SectionId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 2)] + [Parameter(Mandatory = false, Position = 2, HelpMessage = "Description for the subnet.")] public string? Description { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 3)] + [Parameter(Mandatory = false, Position = 3, HelpMessage = "VLAN ID to associate.")] public int? VlanId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 4)] + [Parameter(Mandatory = false, Position = 4, HelpMessage = "VRF ID to associate.")] public int? VrfId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 5)] + [Parameter(Mandatory = false, Position = 5, HelpMessage = "Master subnet ID for hierarchy.")] public int? MasterSubnetId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 6)] + [Parameter(Mandatory = false, Position = 6, HelpMessage = "Nameserver ID.")] public int? NameserverId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 7)] + [Parameter(Mandatory = false, Position = 7, HelpMessage = "Show subnet name.")] public SwitchParameter ShowName { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 8)] + [Parameter(Mandatory = false, Position = 8, HelpMessage = "Enable recursive DNS.")] public SwitchParameter DNSRecursive { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 9)] + [Parameter(Mandatory = false, Position = 9, HelpMessage = "Enable DNS records.")] public SwitchParameter DNSRecords { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 10)] + [Parameter(Mandatory = false, Position = 10, HelpMessage = "Allow IP requests.")] public SwitchParameter AllowRequests { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 11)] + [Parameter(Mandatory = false, Position = 11, HelpMessage = "Scan agent ID.")] public int? ScanAgentId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 12)] + [Parameter(Mandatory = false, Position = 12, HelpMessage = "Enable subnet discovery.")] public SwitchParameter DiscoverSubnet { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 12)] + [Parameter(Mandatory = false, Position = 13, HelpMessage = "Mark subnet as full.")] public SwitchParameter IsFull { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 12)] + [Parameter(Mandatory = false, Position = 14, HelpMessage = "Tag ID for the subnet.")] public int? TagId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 13)] + [Parameter(Mandatory = false, Position = 15, HelpMessage = "Usage threshold percentage (1-100).")] [ValidateRange(1, 100)] public int? Threshold { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 14)] + [Parameter(Mandatory = false, Position = 16, HelpMessage = "Location ID.")] public int? LocationId { get; set; } - [Parameter( - Mandatory = false, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - Position = 15)] + [Parameter(Mandatory = false, HelpMessage = "Custom fields as a hashtable or PSObject.")] public object? CustomFields { get; set; } protected override void ProcessRecord() { try { - var parts = CIDR.Split('/'); - var body = new Dictionary - { - { "subnet", parts[0] }, - { "mask", parts[1] }, - { "sectionId", SectionId } - }; + var body = BuildRequestBody(); - 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; - - if (CustomFields != null) - { - var customDict = ConvertCustomFields(CustomFields); - foreach (var kvp in customDict) - { - body[kvp.Key] = kvp.Value; - } - } - - var result = RequestHelper.InvokeRequest("POST", controllers.subnets, null, null, body, null) - .GetAwaiter().GetResult(); + var result = RequestHelper.InvokeRequest( + "POST", ApiController.Subnets, null, null, body + ).GetAwaiter().GetResult(); if (result != null) { // Get the created subnet - var subnet = RequestHelper.InvokeRequest("GET", controllers.subnets, types.Subnetwork, null, null, new[] { "cidr", CIDR }) - .GetAwaiter().GetResult(); - if (subnet != null) - { - WriteObject(subnet); - } + var subnet = RequestHelper.InvokeRequest( + "GET", ApiController.Subnets, ModelType.Subnetwork, null, null, + new[] { "cidr", CIDR } + ).GetAwaiter().GetResult(); + + WriteResult(subnet); } } catch (Exception ex) @@ -212,20 +107,37 @@ public class NewSubnetCmdlet : PSCmdlet } } - private Dictionary ConvertCustomFields(object customFields) + private Dictionary BuildRequestBody() { - var dict = new Dictionary(); - if (customFields is PSObject psobj) + var parts = CIDR.Split('/'); + var body = new Dictionary { - foreach (var prop in psobj.Properties) - { - dict[prop.Name] = prop.Value ?? new object(); - } - } - else if (customFields is Dictionary dictObj) + ["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)) { - return dictObj; + body[kvp.Key] = kvp.Value; } - return dict; + + return body; } } diff --git a/classlib/Cmdlets/RemoveAddressCmdlet.cs b/classlib/Cmdlets/RemoveAddressCmdlet.cs index 744873b..f064814 100644 --- a/classlib/Cmdlets/RemoveAddressCmdlet.cs +++ b/classlib/Cmdlets/RemoveAddressCmdlet.cs @@ -1,27 +1,31 @@ namespace PS.IPAM.Cmdlets; + using System; -using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; -[Cmdlet(VerbsCommon.Remove, "Address", DefaultParameterSetName = "ByID")] -public class RemoveAddressCmdlet : PSCmdlet +/// +/// Removes an IP address entry from phpIPAM. +/// +[Cmdlet(VerbsCommon.Remove, "Address", DefaultParameterSetName = "ById", SupportsShouldProcess = true)] +public class RemoveAddressCmdlet : BaseCmdlet { - [Parameter( - Mandatory = true, - ValueFromPipeline = true, - Position = 0, - ParameterSetName = "ByID")] - [ValidateNotNullOrEmpty] - public int? Id { get; set; } - [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByAddressObject")] + 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; } @@ -29,19 +33,17 @@ public class RemoveAddressCmdlet : PSCmdlet { try { - int addressId; - if (ParameterSetName == "ByID") - { - addressId = Id!.Value; - } - else - { - addressId = AddressObject!.Id; - } + var addressId = ParameterSetName == "ById" ? Id : AddressObject!.Id; + var identifiers = new[] { addressId.ToString() }; - var identifiers = new List { addressId.ToString() }; - RequestHelper.InvokeRequest("DELETE", controllers.addresses, null, null, null, identifiers.ToArray()) - .GetAwaiter().GetResult(); + 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) { diff --git a/classlib/Cmdlets/SetAddressCmdlet.cs b/classlib/Cmdlets/SetAddressCmdlet.cs index 1a21035..d31c396 100644 --- a/classlib/Cmdlets/SetAddressCmdlet.cs +++ b/classlib/Cmdlets/SetAddressCmdlet.cs @@ -1,20 +1,24 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Updates an existing IP address entry in phpIPAM. +/// [Cmdlet(VerbsCommon.Set, "Address", DefaultParameterSetName = "ById")] [OutputType(typeof(Address))] -public class SetAddressCmdlet : PSCmdlet +public class SetAddressCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ById")] + ParameterSetName = "ById", + HelpMessage = "The address ID to update.")] [ValidateNotNullOrEmpty] public int? Id { get; set; } @@ -23,142 +27,72 @@ public class SetAddressCmdlet : PSCmdlet ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 0, - ParameterSetName = "ByAddressObject")] + ParameterSetName = "ByAddressObject", + HelpMessage = "The address object to update.")] [ValidateNotNullOrEmpty] public Address? AddressObject { get; set; } - [Parameter( - Position = 1, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - ParameterSetName = "ById")] + [Parameter(Position = 1, HelpMessage = "Set as gateway address.")] public bool? Gateway { get; set; } - [Parameter( - Position = 2, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true, - ParameterSetName = "ById")] + [Parameter(Position = 2, HelpMessage = "Description for the address.")] public string? Description { get; set; } - [Parameter( - Mandatory = false, - Position = 3)] + [Parameter(Position = 3, HelpMessage = "Hostname for the address.")] public string? Hostname { get; set; } - [Parameter( - Mandatory = false, - Position = 4)] + [Parameter(Position = 4, HelpMessage = "MAC address.")] public string? MAC { get; set; } - [Parameter( - Mandatory = false, - Position = 5)] + [Parameter(Position = 5, HelpMessage = "Owner of the address.")] public string? Owner { get; set; } - [Parameter( - Mandatory = false, - Position = 6)] + [Parameter(Position = 6, HelpMessage = "Tag ID for the address.")] public int? TagId { get; set; } - [Parameter( - Mandatory = false, - Position = 7)] + [Parameter(Position = 7, HelpMessage = "Ignore PTR record generation.")] public bool? PTRIgnore { get; set; } - [Parameter( - Mandatory = false, - Position = 8)] + [Parameter(Position = 8, HelpMessage = "PTR record ID.")] public int? PTRId { get; set; } - [Parameter( - Mandatory = false, - Position = 9)] + [Parameter(Position = 9, HelpMessage = "Note for the address.")] public string? Note { get; set; } - [Parameter( - Mandatory = false, - Position = 10)] + [Parameter(Position = 10, HelpMessage = "Exclude from ping scanning.")] public bool? ExcludePing { get; set; } - [Parameter( - Mandatory = false, - Position = 11)] + [Parameter(Position = 11, HelpMessage = "Associated device ID.")] public int? DeviceId { get; set; } - [Parameter( - Mandatory = false, - Position = 12)] + [Parameter(Position = 12, HelpMessage = "Port information.")] public string? Port { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(HelpMessage = "Custom fields as a hashtable or PSObject.")] public object? CustomFields { get; set; } protected override void ProcessRecord() { try { - int addressId; - if (ParameterSetName == "ById") - { - addressId = Id!.Value; - } - else - { - addressId = AddressObject!.Id; - } - - var identifiers = new List { addressId.ToString() }; - var body = new Dictionary(); - - 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; - - if (CustomFields != null) - { - var customDict = ConvertCustomFields(CustomFields); - foreach (var kvp in customDict) - { - body[kvp.Key] = kvp.Value; - } - } + var addressId = ParameterSetName == "ById" ? Id!.Value : AddressObject!.Id; + var identifiers = new[] { addressId.ToString() }; + var body = BuildRequestBody(); try { - RequestHelper.InvokeRequest("PATCH", controllers.addresses, null, null, body, identifiers.ToArray()) - .GetAwaiter().GetResult(); + RequestHelper.InvokeRequest( + "PATCH", ApiController.Addresses, null, null, body, identifiers + ).GetAwaiter().GetResult(); } finally { - var address = RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, identifiers.ToArray()) - .GetAwaiter().GetResult(); - if (address != null) - { - WriteObject(address); - } + // Always return the updated address + var address = RequestHelper.InvokeRequest( + "GET", ApiController.Addresses, ModelType.Address, null, null, identifiers + ).GetAwaiter().GetResult(); + + WriteResult(address); } } catch (Exception ex) @@ -167,20 +101,28 @@ public class SetAddressCmdlet : PSCmdlet } } - private Dictionary ConvertCustomFields(object customFields) + private Dictionary BuildRequestBody() { - var dict = new Dictionary(); - if (customFields is PSObject psobj) + var body = new Dictionary(); + + 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)) { - foreach (var prop in psobj.Properties) - { - dict[prop.Name] = prop.Value ?? new object(); - } + body[kvp.Key] = kvp.Value; } - else if (customFields is Dictionary dictObj) - { - return dictObj; - } - return dict; + + return body; } } diff --git a/classlib/Cmdlets/SetSubnetCmdlet.cs b/classlib/Cmdlets/SetSubnetCmdlet.cs index a43521c..12677c6 100644 --- a/classlib/Cmdlets/SetSubnetCmdlet.cs +++ b/classlib/Cmdlets/SetSubnetCmdlet.cs @@ -1,143 +1,91 @@ namespace PS.IPAM.Cmdlets; + using System; using System.Collections.Generic; using System.Management.Automation; -using PS.IPAM; using PS.IPAM.Helpers; +/// +/// Updates an existing subnet in phpIPAM. +/// [Cmdlet(VerbsCommon.Set, "Subnet")] [OutputType(typeof(Subnetwork))] -public class SetSubnetCmdlet : PSCmdlet +public class SetSubnetCmdlet : BaseCmdlet { [Parameter( Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, - Position = 0)] + Position = 0, + HelpMessage = "The subnet ID to update.")] [ValidateNotNullOrEmpty] public int Id { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Description for the subnet.")] public string? Description { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "VLAN ID to associate.")] public int? VlanId { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "VRF ID to associate.")] public int? VrfId { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Master subnet ID for hierarchy.")] public int? MasterSubnetId { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Nameserver ID.")] public int? NameserverId { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Show subnet name.")] public SwitchParameter ShowName { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Enable recursive DNS.")] public SwitchParameter DNSRecursive { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Enable DNS records.")] public SwitchParameter DNSRecords { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Allow IP requests.")] public SwitchParameter AllowRequests { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Scan agent ID.")] public int? ScanAgentId { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Enable subnet discovery.")] public SwitchParameter DiscoverSubnet { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Mark subnet as full.")] public SwitchParameter IsFull { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Tag ID for the subnet.")] public int? TagId { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Usage threshold percentage (1-100).")] [ValidateRange(1, 100)] public int? Threshold { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Location ID.")] public int? LocationId { get; set; } - [Parameter( - Mandatory = false)] + [Parameter(Mandatory = false, HelpMessage = "Custom fields as a hashtable or PSObject.")] public object? CustomFields { get; set; } protected override void ProcessRecord() { try { - var identifiers = new List { Id.ToString() }; - var body = new Dictionary(); + var identifiers = new[] { Id.ToString() }; + var body = BuildRequestBody(); - 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; + RequestHelper.InvokeRequest( + "PATCH", ApiController.Subnets, null, null, body, identifiers + ).GetAwaiter().GetResult(); - if (CustomFields != null) - { - var customDict = ConvertCustomFields(CustomFields); - foreach (var kvp in customDict) - { - body[kvp.Key] = kvp.Value; - } - } + var subnet = RequestHelper.InvokeRequest( + "GET", ApiController.Subnets, ModelType.Subnetwork, null, null, identifiers + ).GetAwaiter().GetResult(); - RequestHelper.InvokeRequest("PATCH", controllers.subnets, null, null, body, identifiers.ToArray()) - .GetAwaiter().GetResult(); - - var subnet = RequestHelper.InvokeRequest("GET", controllers.subnets, types.Subnetwork, null, null, identifiers.ToArray()) - .GetAwaiter().GetResult(); - if (subnet != null) - { - WriteObject(subnet); - } + WriteResult(subnet); } catch (Exception ex) { @@ -145,20 +93,31 @@ public class SetSubnetCmdlet : PSCmdlet } } - private Dictionary ConvertCustomFields(object customFields) + private Dictionary BuildRequestBody() { - var dict = new Dictionary(); - if (customFields is PSObject psobj) + var body = new Dictionary(); + + 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)) { - foreach (var prop in psobj.Properties) - { - dict[prop.Name] = prop.Value ?? new object(); - } + body[kvp.Key] = kvp.Value; } - else if (customFields is Dictionary dictObj) - { - return dictObj; - } - return dict; + + return body; } } diff --git a/classlib/Helpers/RequestHelper.cs b/classlib/Helpers/RequestHelper.cs index 8ee8178..5f846b3 100644 --- a/classlib/Helpers/RequestHelper.cs +++ b/classlib/Helpers/RequestHelper.cs @@ -1,4 +1,5 @@ namespace PS.IPAM.Helpers; + using System; using System.Collections.Generic; using System.Linq; @@ -9,135 +10,259 @@ using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using PS.IPAM; +/// +/// Helper class for making HTTP requests to the phpIPAM API. +/// public static class RequestHelper { - // Handler for testing - allows injecting a mock HTTP handler + /// + /// Handler for testing - allows injecting a mock HTTP handler. + /// public static HttpMessageHandler? TestHttpHandler { get; set; } - public static async Task InvokeRequest( - string method, - controllers controller, - types? type = null, - subcontrollers? subController = null, + /// + /// Invokes an HTTP request to the phpIPAM API. + /// + /// The HTTP method (GET, POST, PATCH, DELETE). + /// The API controller to call. + /// The expected model type for response conversion. + /// Optional sub-controller for nested endpoints. + /// Optional request body parameters. + /// Optional path identifiers. + /// Whether to ignore SSL certificate errors. + /// The deserialized response data, or null if not found. + public static async Task InvokeRequestAsync( + HttpMethod method, + ApiController controller, + ModelType? modelType = null, + ApiSubController? subController = null, object? parameters = null, string[]? identifiers = null, bool ignoreSsl = false) { - var tokenStatus = SessionManager.TestSession(); - if (tokenStatus == "NoToken") + 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) { - throw new Exception("No session available!"); + return null; } - if (tokenStatus == "Expired") + var responseContent = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) { - await UpdateSession(); + 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); + } + + /// + /// Overload for backward compatibility using string method names. + /// + public static Task 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); + } + + /// + /// Refreshes an expired session. + /// + public static async Task RefreshSessionAsync() + { var session = SessionManager.CurrentSession; if (session == null) { - throw new Exception("No session available!"); + throw new InvalidOperationException("No session available!"); } - var uri = $"{session.URL}/api/{session.AppID}/{controller}"; - if (subController != null) + var status = SessionManager.GetSessionStatus(); + if (status == SessionStatus.Valid) { - uri += $"/{subController}"; + // Just refresh the token + await InvokeRequestAsync(new HttpMethod("PATCH"), ApiController.User); + return; } - if (identifiers != null && identifiers.Length > 0) + + 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)}/"; } - using var client = SessionManager.CreateHttpClient(ignoreSsl, TestHttpHandler); + 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: + case AuthType.Credentials: client.DefaultRequestHeaders.Add("token", session.Token); break; - case AuthType.token: + case AuthType.Token: client.DefaultRequestHeaders.Add("phpipam-token", session.Token); break; } + } - HttpResponseMessage? response = null; + private static async Task SendRequestAsync( + HttpClient client, + HttpMethod method, + string uri, + object? parameters) + { try { - if (method == "GET") + if (method == HttpMethod.Get) { - response = await client.GetAsync(uri); - } - else if (method == "POST") - { - var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}"; - var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - response = await client.PostAsync(uri, content); - } - else if (method == "PATCH") - { - var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}"; - var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - var request = new HttpRequestMessage(new HttpMethod("PATCH"), uri) - { - Content = content - }; - response = await client.SendAsync(request); - } - else if (method == "DELETE") - { - response = await client.DeleteAsync(uri); + return await client.GetAsync(uri); } - if (response == null) + if (method == HttpMethod.Delete) { - return null; + return await client.DeleteAsync(uri); } - var responseContent = await response.Content.ReadAsStringAsync(); + var jsonContent = parameters != null ? JsonConvert.SerializeObject(parameters) : "{}"; + var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - if (!response.IsSuccessStatusCode) + if (method == HttpMethod.Post) { - if (response.StatusCode == System.Net.HttpStatusCode.NotFound) - { - return null; - } - throw new HttpRequestException($"Request failed with status {response.StatusCode}"); + return await client.PostAsync(uri, content); } - if (string.IsNullOrEmpty(responseContent)) - { - return null; - } - - var jsonResponse = JsonConvert.DeserializeObject(responseContent); - if (jsonResponse == null) - { - return null; - } - - if (type.HasValue) - { - return ConvertToTypedObjects(jsonResponse, type.Value); - } - - return jsonResponse.data; + // PATCH, PUT, etc. + var request = new HttpRequestMessage(method, uri) { Content = content }; + return await client.SendAsync(request); } - catch (HttpRequestException ex) + catch (HttpRequestException ex) when (ex.Message.Contains("404")) { - if (ex.Message.Contains("404")) - { - return null; - } - throw; + return null; } } - private static object? ConvertToTypedObjects(dynamic jsonResponse, types type) + private static object? ParseResponse(string responseContent, ModelType? modelType) + { + var jsonResponse = JsonConvert.DeserializeObject(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) { @@ -147,228 +272,170 @@ public static class RequestHelper var data = jsonResponse.data; if (data is JArray array) { - return array.Select(item => ConvertSingleObject(item, type)).ToList(); - } - else - { - return ConvertSingleObject(data, type); + return array.Select(item => ConvertSingleObject(item, modelType)).ToList(); } + + return ConvertSingleObject(data, modelType); } - private static object? ConvertSingleObject(dynamic item, types type) + private static object? ConvertSingleObject(dynamic item, ModelType modelType) { - var jobject = item as JObject; - if (jobject == null) + 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() + }; + } + + private static Dictionary? ExtractCustomFields(JObject jobject) + { var customFields = new Dictionary(); - foreach (var prop in jobject.Properties()) + foreach (var prop in jobject.Properties().Where(p => p.Name.StartsWith("custom_"))) { - if (prop.Name.StartsWith("custom_")) - { - customFields[prop.Name] = prop.Value?.ToObject() ?? new object(); - } - } - - switch (type) - { - case types.Address: - return CreateAddress(jobject, customFields); - case types.Vlan: - return CreateVlan(jobject, customFields); - case types.Subnetwork: - return CreateSubnetwork(jobject, customFields); - case types.Vrf: - return CreateVrf(jobject, customFields); - case types.Section: - return CreateSection(jobject); - case types.Tag: - return CreateTag(jobject); - case types.Nameserver: - return CreateNameserver(jobject); - case types.Domain: - return CreateDomain(jobject); - default: - return jobject.ToObject(); + customFields[prop.Name] = prop.Value?.ToObject() ?? string.Empty; } + return customFields.Count > 0 ? customFields : null; } - private static Address CreateAddress(JObject jobject, Dictionary customFields) - { - return new Address( - jobject["id"]?.ToObject() ?? 0, - jobject["subnetId"]?.ToObject() ?? 0, - jobject["ip"]?.ToString() ?? "", - jobject["is_gateway"]?.ToObject() ?? false, - jobject["description"]?.ToString() ?? "", - jobject["hostname"]?.ToString() ?? "", - jobject["mac"]?.ToString() ?? "", - jobject["owner"]?.ToString() ?? "", - jobject["tag"]?.ToObject() ?? 0, - jobject["deviceId"]?.ToObject() ?? 0, - jobject["location"]?.ToString() ?? "", - jobject["port"]?.ToString() ?? "", - jobject["note"]?.ToString() ?? "", - jobject["lastSeen"]?.ToObject(), - jobject["excludePing"]?.ToObject() ?? false, - jobject["PTRignore"]?.ToObject() ?? false, - jobject["PTR"]?.ToObject() ?? 0, - jobject["firewallAddressObject"]?.ToString() ?? "", - jobject["editDate"]?.ToObject(), - jobject["customer_id"]?.ToObject() ?? 0, - customFields.Count > 0 ? customFields : null - ); - } + #region Model Factory Methods - private static Vlan CreateVlan(JObject jobject, Dictionary customFields) - { - return new Vlan( - jobject["vlanId"]?.ToObject() ?? 0, - jobject["domainId"]?.ToObject() ?? 0, - jobject["name"]?.ToString() ?? "", - jobject["number"]?.ToObject() ?? 0, - jobject["description"]?.ToString() ?? "", - jobject["editDate"]?.ToObject(), - jobject["customer_id"]?.ToObject() ?? 0, - customFields.Count > 0 ? customFields : null - ); - } + private static Address CreateAddress(JObject obj, Dictionary? customFields) => new( + obj["id"]?.ToObject() ?? 0, + obj["subnetId"]?.ToObject() ?? 0, + obj["ip"]?.ToString() ?? "", + obj["is_gateway"]?.ToObject() ?? false, + obj["description"]?.ToString() ?? "", + obj["hostname"]?.ToString() ?? "", + obj["mac"]?.ToString() ?? "", + obj["owner"]?.ToString() ?? "", + obj["tag"]?.ToObject() ?? 0, + obj["deviceId"]?.ToObject() ?? 0, + obj["location"]?.ToString() ?? "", + obj["port"]?.ToString() ?? "", + obj["note"]?.ToString() ?? "", + obj["lastSeen"]?.ToObject(), + obj["excludePing"]?.ToObject() ?? false, + obj["PTRignore"]?.ToObject() ?? false, + obj["PTR"]?.ToObject() ?? 0, + obj["firewallAddressObject"]?.ToString() ?? "", + obj["editDate"]?.ToObject(), + obj["customer_id"]?.ToObject() ?? 0, + customFields + ); - private static Subnetwork CreateSubnetwork(JObject jobject, Dictionary customFields) - { - var props = jobject.Properties().ToList(); - return new Subnetwork( - jobject["id"]?.ToObject() ?? 0, - jobject["subnet"]?.ToString() ?? "", - jobject["mask"]?.ToObject() ?? 0, - jobject["sectionId"]?.ToObject() ?? 0, - jobject["description"]?.ToString() ?? "", - jobject["linked_subnet"]?.ToString() ?? "", - jobject["firewallAddressObject"]?.ToString() ?? "", - jobject["vrfId"]?.ToObject() ?? 0, - jobject["masterSubnetId"]?.ToObject() ?? 0, - jobject["allowRequests"]?.ToObject() ?? false, - jobject["vlanId"]?.ToObject() ?? 0, - jobject["showName"]?.ToObject() ?? false, - jobject["deviceId"]?.ToObject() ?? 0, - jobject["permissions"]?.ToString() ?? "", - jobject["pingSubnet"]?.ToObject() ?? false, - jobject["discoverSubnet"]?.ToObject() ?? false, - jobject["resolveDNS"]?.ToObject() ?? false, - jobject["DNSrecursive"]?.ToObject() ?? false, - jobject["DNSrecords"]?.ToObject() ?? false, - jobject["nameserverId"]?.ToObject() ?? 0, - jobject["scanAgent"]?.ToObject() ?? false, - jobject["isFolder"]?.ToObject() ?? false, - jobject["isFull"]?.ToObject() ?? false, - jobject["isPool"]?.ToObject() ?? false, - jobject["state"]?.ToObject() ?? 0, - jobject["threshold"]?.ToObject() ?? 0, - jobject["location"]?.ToObject() ?? 0, - jobject["editDate"]?.ToObject(), - jobject["lastScan"]?.ToObject(), - jobject["lastDiscovery"]?.ToObject(), - jobject["calculation"]?.ToObject() ?? new object(), - customFields.Count > 0 ? customFields : null - ); - } + private static Vlan CreateVlan(JObject obj, Dictionary? customFields) => new( + obj["vlanId"]?.ToObject() ?? 0, + obj["domainId"]?.ToObject() ?? 0, + obj["name"]?.ToString() ?? "", + obj["number"]?.ToObject() ?? 0, + obj["description"]?.ToString() ?? "", + obj["editDate"]?.ToObject(), + obj["customer_id"]?.ToObject() ?? 0, + customFields + ); - private static Vrf CreateVrf(JObject jobject, Dictionary customFields) - { - return new Vrf( - jobject["id"]?.ToObject() ?? 0, - jobject["name"]?.ToString() ?? "", - jobject["rd"]?.ToString() ?? "", - jobject["description"]?.ToString() ?? "", - jobject["sections"]?.ToString() ?? "", - jobject["editDate"]?.ToObject(), - customFields.Count > 0 ? customFields : null - ); - } + private static Subnetwork CreateSubnetwork(JObject obj, Dictionary? customFields) => new( + obj["id"]?.ToObject() ?? 0, + obj["subnet"]?.ToString() ?? "", + obj["mask"]?.ToObject() ?? 0, + obj["sectionId"]?.ToObject() ?? 0, + obj["description"]?.ToString() ?? "", + obj["linked_subnet"]?.ToString() ?? "", + obj["firewallAddressObject"]?.ToString() ?? "", + obj["vrfId"]?.ToObject() ?? 0, + obj["masterSubnetId"]?.ToObject() ?? 0, + obj["allowRequests"]?.ToObject() ?? false, + obj["vlanId"]?.ToObject() ?? 0, + obj["showName"]?.ToObject() ?? false, + obj["deviceId"]?.ToObject() ?? 0, + obj["permissions"]?.ToString() ?? "", + obj["pingSubnet"]?.ToObject() ?? false, + obj["discoverSubnet"]?.ToObject() ?? false, + obj["resolveDNS"]?.ToObject() ?? false, + obj["DNSrecursive"]?.ToObject() ?? false, + obj["DNSrecords"]?.ToObject() ?? false, + obj["nameserverId"]?.ToObject() ?? 0, + obj["scanAgent"]?.ToObject() ?? false, + obj["isFolder"]?.ToObject() ?? false, + obj["isFull"]?.ToObject() ?? false, + obj["isPool"]?.ToObject() ?? false, + obj["state"]?.ToObject() ?? 0, + obj["threshold"]?.ToObject() ?? 0, + obj["location"]?.ToObject() ?? 0, + obj["editDate"]?.ToObject(), + obj["lastScan"]?.ToObject(), + obj["lastDiscovery"]?.ToObject(), + obj["calculation"]?.ToObject() ?? new object(), + customFields + ); - private static Section CreateSection(JObject jobject) - { - return new Section( - jobject["id"]?.ToObject() ?? 0, - jobject["name"]?.ToString() ?? "", - jobject["description"]?.ToString() ?? "", - jobject["masterSection"]?.ToObject() ?? 0, - jobject["permissions"]?.ToString() ?? "", - jobject["strictMode"]?.ToObject() ?? false, - jobject["subnetOrdering"]?.ToString() ?? "", - jobject["order"]?.ToObject() ?? 0, - jobject["editDate"]?.ToObject(), - jobject["showSubnet"]?.ToObject() ?? false, - jobject["showVlan"]?.ToObject() ?? false, - jobject["showVRF"]?.ToObject() ?? false, - jobject["showSupernetOnly"]?.ToObject() ?? false, - jobject["DNS"]?.ToObject() ?? 0 - ); - } + private static Vrf CreateVrf(JObject obj, Dictionary? customFields) => new( + obj["id"]?.ToObject() ?? 0, + obj["name"]?.ToString() ?? "", + obj["rd"]?.ToString() ?? "", + obj["description"]?.ToString() ?? "", + obj["sections"]?.ToString() ?? "", + obj["editDate"]?.ToObject(), + customFields + ); - private static Tag CreateTag(JObject jobject) - { - return new Tag( - jobject["id"]?.ToObject() ?? 0, - jobject["type"]?.ToString() ?? "", - jobject["showtag"]?.ToObject() ?? false, - jobject["bgcolor"]?.ToString() ?? "", - jobject["fgcolor"]?.ToString() ?? "", - jobject["compress"]?.ToString() ?? "", - jobject["locked"]?.ToString() ?? "", - jobject["updateTag"]?.ToObject() ?? false - ); - } + private static Section CreateSection(JObject obj) => new( + obj["id"]?.ToObject() ?? 0, + obj["name"]?.ToString() ?? "", + obj["description"]?.ToString() ?? "", + obj["masterSection"]?.ToObject() ?? 0, + obj["permissions"]?.ToString() ?? "", + obj["strictMode"]?.ToObject() ?? false, + obj["subnetOrdering"]?.ToString() ?? "", + obj["order"]?.ToObject() ?? 0, + obj["editDate"]?.ToObject(), + obj["showSubnet"]?.ToObject() ?? false, + obj["showVlan"]?.ToObject() ?? false, + obj["showVRF"]?.ToObject() ?? false, + obj["showSupernetOnly"]?.ToObject() ?? false, + obj["DNS"]?.ToObject() ?? 0 + ); - private static Nameserver CreateNameserver(JObject jobject) - { - return new Nameserver( - jobject["id"]?.ToObject() ?? 0, - jobject["name"]?.ToString() ?? "", - jobject["nameservers"]?.ToString() ?? "", - jobject["description"]?.ToString() ?? "", - jobject["permissions"]?.ToString() ?? "", - jobject["editDate"]?.ToObject() - ); - } + private static Tag CreateTag(JObject obj) => new( + obj["id"]?.ToObject() ?? 0, + obj["type"]?.ToString() ?? "", + obj["showtag"]?.ToObject() ?? false, + obj["bgcolor"]?.ToString() ?? "", + obj["fgcolor"]?.ToString() ?? "", + obj["compress"]?.ToString() ?? "", + obj["locked"]?.ToString() ?? "", + obj["updateTag"]?.ToObject() ?? false + ); - private static Domain CreateDomain(JObject jobject) - { - return new Domain( - jobject["id"]?.ToObject() ?? 0, - jobject["name"]?.ToString() ?? "", - jobject["description"]?.ToString() ?? "", - jobject["sections"]?.ToString() ?? "" - ); - } + private static Nameserver CreateNameserver(JObject obj) => new( + obj["id"]?.ToObject() ?? 0, + obj["name"]?.ToString() ?? "", + obj["nameservers"]?.ToString() ?? "", + obj["description"]?.ToString() ?? "", + obj["permissions"]?.ToString() ?? "", + obj["editDate"]?.ToObject() + ); - private static async Task UpdateSession() - { - var session = SessionManager.CurrentSession; - if (session == null) - { - throw new Exception("No session available!"); - } + private static Domain CreateDomain(JObject obj) => new( + obj["id"]?.ToObject() ?? 0, + obj["name"]?.ToString() ?? "", + obj["description"]?.ToString() ?? "", + obj["sections"]?.ToString() ?? "" + ); - var tokenStatus = SessionManager.TestSession(); - if (tokenStatus == "Valid") - { - // Just refresh the token - var result = await InvokeRequest("PATCH", controllers.user, null, null, null, null); - // Token refresh doesn't return expires in the same format, so we'll skip updating expires - return; - } - - if (tokenStatus == "Expired" && session.Credentials is PSCredential creds) - { - await SessionManager.CreateSessionWithCredentials( - session.URL, - session.AppID, - creds, - false - ); - } - } + #endregion } diff --git a/classlib/Helpers/SessionManager.cs b/classlib/Helpers/SessionManager.cs index 4df215b..ff5c045 100644 --- a/classlib/Helpers/SessionManager.cs +++ b/classlib/Helpers/SessionManager.cs @@ -1,44 +1,60 @@ 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; -using PS.IPAM; +/// +/// Manages phpIPAM API sessions including creation, validation, and lifecycle. +/// public static class SessionManager { private static Session? _currentSession; + /// + /// Gets or sets the current active session. + /// public static Session? CurrentSession { get => _currentSession; set => _currentSession = value; } - public static string TestSession() + /// + /// Tests the current session status. + /// + /// The session status indicating validity or issues. + public static SessionStatus GetSessionStatus() { if (_currentSession == null) { - return "NoToken"; + return SessionStatus.NoSession; } if (_currentSession.Expires == null) { - return "Valid"; + return SessionStatus.Valid; } - if (_currentSession.Expires < DateTime.Now) - { - return "Expired"; - } - - return "Valid"; + return _currentSession.Expires < DateTime.Now + ? SessionStatus.Expired + : SessionStatus.Valid; } - public static async Task CreateSessionWithCredentials( + /// + /// Creates a new session using username/password credentials. + /// + /// The phpIPAM server URL. + /// The API application ID. + /// The PowerShell credential object. + /// Whether to ignore SSL certificate errors. + /// The created session. + public static async Task CreateSessionWithCredentialsAsync( string url, string appId, PSCredential credentials, @@ -46,7 +62,7 @@ public static class SessionManager { var uri = $"{url}/api/{appId}/user"; var auth = Convert.ToBase64String( - Encoding.UTF8.GetBytes($"{credentials.UserName}:{GetPassword(credentials)}")); + Encoding.UTF8.GetBytes($"{credentials.UserName}:{GetPasswordString(credentials)}")); using var client = CreateHttpClient(ignoreSsl); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); @@ -58,14 +74,15 @@ public static class SessionManager if (jsonResponse?.success != true) { - throw new Exception(jsonResponse?.error?.ToString() ?? "Failed to create session"); + 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, + AuthType.Credentials, token, appId, url, @@ -76,13 +93,17 @@ public static class SessionManager return _currentSession; } - public static Session CreateSessionWithToken( - string url, - string appId, - string token) + /// + /// Creates a new session using a static API token. + /// + /// The phpIPAM server URL. + /// The API application ID. + /// The API token. + /// The created session. + public static Session CreateSessionWithToken(string url, string appId, string token) { _currentSession = new Session( - AuthType.token, + AuthType.Token, token, appId, url, @@ -93,24 +114,20 @@ public static class SessionManager return _currentSession; } + /// + /// Closes the current session and clears session data. + /// public static void CloseSession() { _currentSession = null; } - private static string GetPassword(PSCredential credential) - { - var ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(credential.Password); - try - { - return System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ptr); - } - finally - { - System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ptr); - } - } - + /// + /// Creates an HttpClient with optional SSL bypass. + /// + /// Whether to ignore SSL certificate errors. + /// Optional custom message handler for testing. + /// A configured HttpClient instance. public static HttpClient CreateHttpClient(bool ignoreSsl = false, HttpMessageHandler? handler = null) { if (handler != null) @@ -122,10 +139,27 @@ public static class SessionManager { var sslHandler = new HttpClientHandler { - ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true + ServerCertificateCustomValidationCallback = (_, _, _, _) => true }; return new HttpClient(sslHandler); } + return new HttpClient(); } + + /// + /// Extracts the plain text password from a PSCredential object. + /// + private static string GetPasswordString(PSCredential credential) + { + var ptr = Marshal.SecureStringToBSTR(credential.Password); + try + { + return Marshal.PtrToStringBSTR(ptr); + } + finally + { + Marshal.ZeroFreeBSTR(ptr); + } + } } diff --git a/classlib/class/address.cs b/classlib/class/address.cs index d2044c9..80b3d56 100644 --- a/classlib/class/address.cs +++ b/classlib/class/address.cs @@ -1,77 +1,35 @@ -namespace PS.IPAM; +namespace PS.IPAM; + using System; +using System.Collections.Generic; +/// +/// Represents an IP address entry in phpIPAM. +/// [Serializable] -public class Address { - public int Id { get; } - public int SubnetId { get; } - public string Ip { get; } - public bool IsGateway { get; } - public string Description { get; } - public string Hostname { get; } - public string MAC { get; } - public string Owner { get; } - public int TagId { get; } - public int DeviceId { get; } - public string Location { get; } - public string Port { get; } - public string Note { get; } - public DateTime? LastSeen { get; } - public bool ExcludePing { get; } - public bool PTRignore { get; } - 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, - string firewallAddressObject, - DateTime? editDate, - int customer_id, - Object? extendedData - ) { - this.Id = id; - this.SubnetId = subnetId; - 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.LastSeen = 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; - } -} \ No newline at end of file +public sealed record Address( + int Id, + int SubnetId, + string Ip, + bool IsGateway, + string Description, + string Hostname, + string MAC, + string Owner, + int TagId, + int DeviceId, + string Location, + string Port, + string Note, + DateTime? LastSeen, + bool ExcludePing, + bool PTRIgnore, + int PTR, + string FirewallAddressObject, + DateTime? EditDate, + int CustomerId, + Dictionary? ExtendedData = null +) +{ + public override string ToString() => Ip; +} diff --git a/classlib/class/domain.cs b/classlib/class/domain.cs index dc8a184..ca6322c 100644 --- a/classlib/class/domain.cs +++ b/classlib/class/domain.cs @@ -1,27 +1,17 @@ namespace PS.IPAM; + using System; +/// +/// Represents an L2 domain in phpIPAM. +/// [Serializable] -public class Domain { - public int Id { get; } - public string Name { get; } - public string Description { get; } - public string Sections { get; } - - public Domain ( - int id, - 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; - } -} \ No newline at end of file +public sealed record Domain( + int Id, + string Name, + string Description, + string Sections +) +{ + public override string ToString() => Name; +} diff --git a/classlib/class/nameserver.cs b/classlib/class/nameserver.cs index d692032..c11baaf 100644 --- a/classlib/class/nameserver.cs +++ b/classlib/class/nameserver.cs @@ -1,8 +1,13 @@ namespace PS.IPAM; + using System; +/// +/// Represents a nameserver configuration in phpIPAM. +/// [Serializable] -public class Nameserver { +public sealed record Nameserver +{ public int Id { get; } public string Name { get; } public string[] NameServers { get; } @@ -16,13 +21,15 @@ public class Nameserver { string nameServers, string description, string permissions, - DateTime? editDate - ) { - this.Id = id; - this.Name = name; - this.NameServers = nameServers.Split(new char[] {';'}); - this.Description = description; - this.Permissions = permissions; - this.EditDate = editDate; + DateTime? editDate) + { + Id = id; + Name = name; + NameServers = nameServers?.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); + Description = description; + Permissions = permissions; + EditDate = editDate; } -} \ No newline at end of file + + public override string ToString() => Name; +} diff --git a/classlib/class/section.cs b/classlib/class/section.cs index 28022d1..eff437e 100644 --- a/classlib/class/section.cs +++ b/classlib/class/section.cs @@ -1,57 +1,27 @@ namespace PS.IPAM; + using System; +/// +/// Represents a section in phpIPAM that organizes subnets. +/// [Serializable] -public class Section { - public int Id { get; } - public string Name { get; } - public string Description { get; } - public int MasterSectionId { get; } - public string Permissions { get; } - public bool StrictMode { get; } - public string SubnetOrdering { get; } - public int Order { get; } - public DateTime? EditDate { get; } - public bool ShowSubnet { get; } - public bool ShowVlan { get; } - public bool ShowVRF { get; } - public bool ShowSupernetOnly { get; } - public int DNSId { get; } - - public Section ( - int id, - 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; - } -} \ No newline at end of file +public sealed record Section( + int Id, + 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 +) +{ + public override string ToString() => Name; +} diff --git a/classlib/class/session.cs b/classlib/class/session.cs index a56e056..576a37b 100644 --- a/classlib/class/session.cs +++ b/classlib/class/session.cs @@ -1,33 +1,27 @@ namespace PS.IPAM; + using System; -using System.Management.Automation; +/// +/// Represents an authenticated session with phpIPAM API. +/// [Serializable] -public class Session { - public AuthType AuthType { get; } - public string Token { get; set; } - public string AppID { get; } - public string URL { get; } - public DateTime? Expires { get; set; } - public object? Credentials { get; } - public Session( - AuthType authType, - string token, - string appId, - string url, - DateTime? expires, - object? credentials - ) { - AuthType = authType; - Token = token; - AppID = appId; - URL = url; - Expires = expires; - Credentials = credentials; - } +public sealed record Session( + AuthType AuthType, + string Token, + string AppID, + string URL, + DateTime? Expires, + object? Credentials +) +{ + /// + /// Gets or sets the current authentication token. + /// + public string Token { get; set; } = Token; - public override string ToString() - { - return base.ToString(); - } -} \ No newline at end of file + /// + /// Gets or sets the token expiration time. + /// + public DateTime? Expires { get; set; } = Expires; +} diff --git a/classlib/class/subnet.cs b/classlib/class/subnet.cs index 9785ff3..8afbef5 100644 --- a/classlib/class/subnet.cs +++ b/classlib/class/subnet.cs @@ -1,114 +1,51 @@ namespace PS.IPAM; + using System; +using System.Collections.Generic; +/// +/// Represents a subnet/network in phpIPAM. +/// [Serializable] -public class Subnetwork { - public int Id { get; } - public string Subnet { get; } - public int Mask { get; } - public int SectionId { get; } - public string Description { get; } - public string LinkedSubnet { get; } - public string FirewallAddressObject { get; } - public int VrfId { get; } - public int MasterSubnetId { get; } - public bool AllowRequests { get; } - public int VlanId { get; } - public bool ShowName { get; } - public int DeviceId { get; } - public string Permissions { get; } - public bool PingSubnet { get; } - public bool DiscoverSubnet { get; } - public bool ResolveDNS { get; } - public bool DNSRecursive { get; } - public bool DNSRecords { get; } - public int NameserverId { get; } - public bool ScanAgent { get; } - public bool IsFolder { get; } - public bool IsFull { get; } - public bool IsPool { get; } - public int TagId { get; } - public int Threshold { get; } - public int LocationId { get; } - public DateTime? EditDate { get; } - public DateTime? LastScan { get; } - public DateTime? LastDiscovery { get; } - public Object Calculation { get; } - public Object? ExtendedData { get; } - public Subnetwork( - int id, - string subnet, - int mask, - int sectionId, - string description, - 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 sealed record Subnetwork( + int Id, + string Subnet, + int Mask, + int SectionId, + string Description, + 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, + Dictionary? ExtendedData = null +) +{ + /// + /// Gets the subnet in CIDR notation (e.g., "192.168.1.0/24"). + /// + public string CIDR => $"{Subnet}/{Mask}"; - public string GetCIDR() { - return $"{this.Subnet}/{this.Mask}"; - } - - public override string ToString() - { - return this.GetCIDR(); - } -} \ No newline at end of file + public override string ToString() => CIDR; +} diff --git a/classlib/class/tag.cs b/classlib/class/tag.cs index ddc94e5..6231e37 100644 --- a/classlib/class/tag.cs +++ b/classlib/class/tag.cs @@ -1,45 +1,21 @@ namespace PS.IPAM; + using System; +/// +/// Represents an address tag in phpIPAM. +/// [Serializable] -public class Tag { - public int Id { get; } - public string Type { get; } - public bool ShowTag { get; } - public string BGColor { get; } - public string FGColor { get; } - public bool Compress { get; } - public bool Locked { get; } - public bool UpdateTag { get; } - - public Tag( - int id, - 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; - } -} \ No newline at end of file +public sealed record Tag( + int Id, + string Type, + bool ShowTag, + string BackgroundColor, + string ForegroundColor, + string Compress, + string Locked, + bool UpdateTag +) +{ + public override string ToString() => Type; +} diff --git a/classlib/class/vlan.cs b/classlib/class/vlan.cs index ec080b9..b9ea921 100644 --- a/classlib/class/vlan.cs +++ b/classlib/class/vlan.cs @@ -1,41 +1,27 @@ namespace PS.IPAM; + using System; -using System.Dynamic; +using System.Collections.Generic; +/// +/// Represents a VLAN in phpIPAM. +/// [Serializable] -public class Vlan : DynamicObject { - public int Id { get; } - public int VlanId { get; } - public int DomainId { get; } - public string Name { get; } - public int Number { get; } - public string Description { get; } - public DateTime? EditDate { get; } - public int CustomerId { get; } - public Object? ExtendedData { get; } - public Vlan ( - int vlanId, - int domainId, - string name, - int number, - string description, - DateTime? editDate, - int customer_id, - Object? custom_fields - ) { - this.Id = vlanId; - this.VlanId = 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 sealed record Vlan( + int Id, + int DomainId, + string Name, + int Number, + string Description, + DateTime? EditDate, + int CustomerId, + Dictionary? ExtendedData = null +) +{ + /// + /// Alias for Id to maintain API compatibility. + /// + public int VlanId => Id; - public override string ToString() - { - return $"{this.Number}"; - } -} \ No newline at end of file + public override string ToString() => Number.ToString(); +} diff --git a/classlib/class/vrf.cs b/classlib/class/vrf.cs index 2c426ac..e925e17 100644 --- a/classlib/class/vrf.cs +++ b/classlib/class/vrf.cs @@ -1,35 +1,21 @@ namespace PS.IPAM; + using System; +using System.Collections.Generic; +/// +/// Represents a VRF (Virtual Routing and Forwarding) instance in phpIPAM. +/// [Serializable] -public class Vrf { - public int Id { get; } - public string Name { get; } - public string RouteDistinguisher { get; } - public string Description { get; } - public string Sections { get; } - public DateTime? EditDate { get; } - public Object? ExtendedData { get; } - - public Vrf( - int id, - 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; - } -} \ No newline at end of file +public sealed record Vrf( + int Id, + string Name, + string RouteDistinguisher, + string Description, + string Sections, + DateTime? EditDate, + Dictionary? ExtendedData = null +) +{ + public override string ToString() => Name; +} diff --git a/classlib/enum/authType.cs b/classlib/enum/authType.cs index bb513c6..6f5f499 100644 --- a/classlib/enum/authType.cs +++ b/classlib/enum/authType.cs @@ -1,8 +1,20 @@ namespace PS.IPAM; + using System; +/// +/// Specifies the authentication method for phpIPAM API. +/// [Serializable] -public enum AuthType { - credentials, - token -} \ No newline at end of file +public enum AuthType +{ + /// + /// Authentication using username and password credentials. + /// + Credentials, + + /// + /// Authentication using a static API token. + /// + Token +} diff --git a/classlib/enum/controllers.cs b/classlib/enum/controllers.cs index 7d6c6a6..1a0f597 100644 --- a/classlib/enum/controllers.cs +++ b/classlib/enum/controllers.cs @@ -1,14 +1,19 @@ namespace PS.IPAM; + using System; +/// +/// Represents the main API controllers in phpIPAM. +/// [Serializable] -public enum controllers { - user, - vlan, - subnets, - addresses, - sections, - vrf, - l2domains, - tools -} \ No newline at end of file +public enum ApiController +{ + User, + Vlan, + Subnets, + Addresses, + Sections, + Vrf, + L2Domains, + Tools +} diff --git a/classlib/enum/subcontrollers.cs b/classlib/enum/subcontrollers.cs index a674d67..bc148d4 100644 --- a/classlib/enum/subcontrollers.cs +++ b/classlib/enum/subcontrollers.cs @@ -1,16 +1,21 @@ namespace PS.IPAM; + using System; +/// +/// Represents sub-controllers/endpoints within main API controllers. +/// [Serializable] -public enum subcontrollers { - nameservers, - tags, - devices, - device_types, - vlans, - vrfs, - scanagents, - locations, - nat, - racks -} \ No newline at end of file +public enum ApiSubController +{ + Nameservers, + Tags, + Devices, + DeviceTypes, + Vlans, + Vrfs, + ScanAgents, + Locations, + Nat, + Racks +} diff --git a/classlib/enum/types.cs b/classlib/enum/types.cs index 6bc4a56..2424633 100644 --- a/classlib/enum/types.cs +++ b/classlib/enum/types.cs @@ -1,8 +1,13 @@ namespace PS.IPAM; + using System; +/// +/// Represents the model types returned by the phpIPAM API. +/// [Serializable] -public enum types { +public enum ModelType +{ Address, Domain, Section, @@ -11,4 +16,4 @@ public enum types { Tag, Vlan, Vrf -} \ No newline at end of file +} From bc86ad2965a9823dcae2f640e28186c1b9ef4edf Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 17:25:41 +0300 Subject: [PATCH 4/4] Add IsExternalInit type, BaseCmdlet class, and SessionStatus enum 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. --- classlib/Cmdlets/BaseCmdlet.cs | 114 +++++++++++++++++++++++++++++++++ classlib/IsExternalInit.cs | 23 +++++++ classlib/enum/SessionStatus.cs | 22 +++++++ 3 files changed, 159 insertions(+) create mode 100644 classlib/Cmdlets/BaseCmdlet.cs create mode 100644 classlib/IsExternalInit.cs create mode 100644 classlib/enum/SessionStatus.cs diff --git a/classlib/Cmdlets/BaseCmdlet.cs b/classlib/Cmdlets/BaseCmdlet.cs new file mode 100644 index 0000000..83dc9d3 --- /dev/null +++ b/classlib/Cmdlets/BaseCmdlet.cs @@ -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; + +/// +/// Base class for all PS.IPAM cmdlets providing common functionality. +/// +public abstract class BaseCmdlet : PSCmdlet +{ + /// + /// Writes the result to the pipeline, handling both single objects and collections. + /// + /// The result to write to the pipeline. + 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); + } + } + + /// + /// Converts custom fields from various formats to a dictionary. + /// + /// The custom fields object to convert. + /// A dictionary of custom field names and values. + protected static Dictionary ConvertCustomFields(object? customFields) + { + if (customFields == null) + { + return new Dictionary(); + } + + if (customFields is PSObject psobj) + { + var dict = new Dictionary(); + foreach (var prop in psobj.Properties) + { + dict[prop.Name] = prop.Value ?? string.Empty; + } + return dict; + } + + if (customFields is Dictionary dictObj) + { + return dictObj; + } + + if (customFields is IDictionary genericDict) + { + var result = new Dictionary(); + foreach (DictionaryEntry entry in genericDict) + { + result[entry.Key?.ToString() ?? string.Empty] = entry.Value ?? string.Empty; + } + return result; + } + + return new Dictionary(); + } + + /// + /// Executes an async operation synchronously and handles errors. + /// + /// The return type of the operation. + /// The async operation to execute. + /// The error ID for error reporting. + /// The result of the operation, or default if an error occurred. + protected T? ExecuteAsync(Func> operation, string errorId) + { + try + { + return operation().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, errorId, ErrorCategory.InvalidOperation, null)); + return default; + } + } + + /// + /// Executes an async operation synchronously and handles errors. + /// + /// The async operation to execute. + /// The error ID for error reporting. + protected void ExecuteAsync(Func operation, string errorId) + { + try + { + operation().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, errorId, ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/IsExternalInit.cs b/classlib/IsExternalInit.cs new file mode 100644 index 0000000..85ba17f --- /dev/null +++ b/classlib/IsExternalInit.cs @@ -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; + + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} + +#endif diff --git a/classlib/enum/SessionStatus.cs b/classlib/enum/SessionStatus.cs new file mode 100644 index 0000000..0c9c73c --- /dev/null +++ b/classlib/enum/SessionStatus.cs @@ -0,0 +1,22 @@ +namespace PS.IPAM; + +/// +/// Represents the current status of an API session. +/// +public enum SessionStatus +{ + /// + /// No session exists or no token is available. + /// + NoSession, + + /// + /// The session token has expired. + /// + Expired, + + /// + /// The session is valid and ready for use. + /// + Valid +}