From f8f240e31326208d5837c5d54cfda80c70f3a6c4 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 14:44:10 +0300 Subject: [PATCH 1/6] Updated subnet object handling and improved documentation --- .../Cmdlets/GetAddressCmdletTests.cs | 323 ++++++++++++++++++ .../Cmdlets/NewSessionCmdletTests.cs | 103 ++++++ 2 files changed, 426 insertions(+) create mode 100644 classlib.tests/Cmdlets/GetAddressCmdletTests.cs create mode 100644 classlib.tests/Cmdlets/NewSessionCmdletTests.cs diff --git a/classlib.tests/Cmdlets/GetAddressCmdletTests.cs b/classlib.tests/Cmdlets/GetAddressCmdletTests.cs new file mode 100644 index 0000000..8ca88e5 --- /dev/null +++ b/classlib.tests/Cmdlets/GetAddressCmdletTests.cs @@ -0,0 +1,323 @@ +namespace PS.IPAM.Tests.Cmdlets; + +using System.Net; +using FluentAssertions; +using PS.IPAM; +using PS.IPAM.Cmdlets; +using PS.IPAM.Helpers; +using PS.IPAM.Tests.Mocks; +using Xunit; + +/// +/// Tests for the GetAddressCmdlet. +/// Note: Full cmdlet testing with parameter sets requires a PowerShell runspace. +/// These tests focus on verifying the cmdlet structure and the underlying RequestHelper functionality. +/// +[Collection("Sequential")] +public class GetAddressCmdletTests : IDisposable +{ + private MockHttpMessageHandler? _mockHandler; + + public GetAddressCmdletTests() + { + // Clean state before each test + SessionManager.CloseSession(); + RequestHelper.TestHttpHandler = null; + } + + public void Dispose() + { + // Clean up after each test + SessionManager.CloseSession(); + RequestHelper.TestHttpHandler = null; + _mockHandler?.ForceDispose(); + } + + private void SetupSession() + { + SessionManager.CreateSessionWithToken("https://ipam.example.com", "testapp", "test-token"); + } + + private MockHttpMessageHandler SetupMockHandler() + { + _mockHandler = new MockHttpMessageHandler(); + RequestHelper.TestHttpHandler = _mockHandler; + return _mockHandler; + } + + [Fact] + public void GetAddressCmdlet_Exists() + { + // Verify the cmdlet class exists and can be instantiated + var cmdlet = new GetAddressCmdlet(); + cmdlet.Should().NotBeNull(); + } + + [Fact] + public void GetAddressCmdlet_IdProperty_Exists() + { + var cmdlet = new GetAddressCmdlet(); + cmdlet.Id = 42; + cmdlet.Id.Should().Be(42); + } + + [Fact] + public void GetAddressCmdlet_IPProperty_Exists() + { + var cmdlet = new GetAddressCmdlet(); + cmdlet.IP = IPAddress.Parse("192.168.1.100"); + cmdlet.IP.Should().Be(IPAddress.Parse("192.168.1.100")); + } + + [Fact] + public void GetAddressCmdlet_HostNameProperty_Exists() + { + var cmdlet = new GetAddressCmdlet(); + cmdlet.HostName = "server01.example.com"; + cmdlet.HostName.Should().Be("server01.example.com"); + } + + [Fact] + public void GetAddressCmdlet_HostBaseProperty_Exists() + { + var cmdlet = new GetAddressCmdlet(); + cmdlet.HostBase = "server"; + cmdlet.HostBase.Should().Be("server"); + } + + [Fact] + public void GetAddressCmdlet_TagIdProperty_Exists() + { + var cmdlet = new GetAddressCmdlet(); + cmdlet.TagId = 2; + cmdlet.TagId.Should().Be(2); + } + + [Fact] + public void GetAddressCmdlet_SubnetIdProperty_Exists() + { + var cmdlet = new GetAddressCmdlet(); + cmdlet.SubnetId = 10; + cmdlet.SubnetId.Should().Be(10); + } + + [Fact] + public void GetAddressCmdlet_SubnetCIDRProperty_Exists() + { + var cmdlet = new GetAddressCmdlet(); + cmdlet.SubnetCIDR = "192.168.1.0/24"; + cmdlet.SubnetCIDR.Should().Be("192.168.1.0/24"); + } + + // Test the underlying RequestHelper functionality that the cmdlet uses + + [Fact] + public async Task RequestHelper_GetAddressById_ReturnsAddress() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var addressJson = @"{ + ""id"": 1, + ""subnetId"": 10, + ""ip"": ""192.168.1.100"", + ""hostname"": ""server01"", + ""description"": ""Test server"" + }"; + handler.WithSuccessResponse(addressJson); + + // Act + var result = await RequestHelper.InvokeRequest( + "GET", + controllers.addresses, + types.Address, + null, + null, + new[] { "1" } + ); + + // Assert + result.Should().BeOfType
(); + var address = (Address)result!; + address.Id.Should().Be(1); + address.Ip.Should().Be("192.168.1.100"); + address.Hostname.Should().Be("server01"); + } + + [Fact] + public async Task RequestHelper_GetAddressById_BuildsCorrectUri() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"{""id"": 42, ""ip"": ""10.0.0.1""}"); + + // Act + await RequestHelper.InvokeRequest( + "GET", + controllers.addresses, + types.Address, + null, + null, + new[] { "42" } + ); + + // Assert + handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/42/"); + } + + [Fact] + public async Task RequestHelper_SearchByIP_BuildsCorrectUri() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"{""id"": 1, ""ip"": ""192.168.1.50""}"); + + // Act + await RequestHelper.InvokeRequest( + "GET", + controllers.addresses, + types.Address, + null, + null, + new[] { "search", "192.168.1.50" } + ); + + // Assert + handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/search/192.168.1.50/"); + } + + [Fact] + public async Task RequestHelper_SearchByHostname_BuildsCorrectUri() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"{""id"": 1, ""ip"": ""10.0.0.5"", ""hostname"": ""myserver.example.com""}"); + + // Act + await RequestHelper.InvokeRequest( + "GET", + controllers.addresses, + types.Address, + null, + null, + new[] { "search_hostname", "myserver.example.com" } + ); + + // Assert + handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/search_hostname/myserver.example.com/"); + } + + [Fact] + public async Task RequestHelper_GetSubnetAddresses_BuildsCorrectUri() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"[{""id"": 1, ""ip"": ""192.168.1.1""}]"); + + // Act + await RequestHelper.InvokeRequest( + "GET", + controllers.subnets, + types.Address, + null, + null, + new[] { "10", "addresses" } + ); + + // Assert + handler.LastRequest!.RequestUri!.ToString().Should().Contain("/subnets/10/addresses/"); + } + + [Fact] + public async Task RequestHelper_GetAddressesByTag_BuildsCorrectUri() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"[{""id"": 1, ""ip"": ""10.0.0.1"", ""tag"": 2}]"); + + // Act + await RequestHelper.InvokeRequest( + "GET", + controllers.addresses, + types.Address, + null, + null, + new[] { "tags", "2", "addresses" } + ); + + // Assert + handler.LastRequest!.RequestUri!.ToString().Should().Contain("/addresses/tags/2/addresses/"); + } + + [Fact] + public async Task RequestHelper_ReturnsMultipleAddresses() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"[ + {""id"": 1, ""ip"": ""192.168.1.1""}, + {""id"": 2, ""ip"": ""192.168.1.2""}, + {""id"": 3, ""ip"": ""192.168.1.3""} + ]"); + + // Act + var result = await RequestHelper.InvokeRequest( + "GET", + controllers.subnets, + types.Address, + null, + null, + new[] { "10", "addresses" } + ); + + // Assert + result.Should().BeAssignableTo(); + var addresses = ((System.Collections.IEnumerable)result!).Cast().ToList(); + addresses.Should().HaveCount(3); + addresses.Should().AllBeOfType
(); + } + + [Fact] + public async Task RequestHelper_With404Response_ReturnsNull() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithNotFoundResponse(); + + // Act + var result = await RequestHelper.InvokeRequest( + "GET", + controllers.addresses, + types.Address, + null, + null, + new[] { "999" } + ); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task RequestHelper_WithNoSession_ThrowsException() + { + // Arrange - no session set up + + // Act + var action = async () => await RequestHelper.InvokeRequest( + "GET", + controllers.addresses, + types.Address + ); + + // Assert + await action.Should().ThrowAsync().WithMessage("No session available!"); + } +} diff --git a/classlib.tests/Cmdlets/NewSessionCmdletTests.cs b/classlib.tests/Cmdlets/NewSessionCmdletTests.cs new file mode 100644 index 0000000..64c8f71 --- /dev/null +++ b/classlib.tests/Cmdlets/NewSessionCmdletTests.cs @@ -0,0 +1,103 @@ +namespace PS.IPAM.Tests.Cmdlets; + +using FluentAssertions; +using PS.IPAM; +using PS.IPAM.Cmdlets; +using PS.IPAM.Helpers; +using Xunit; + +/// +/// Tests for the NewSessionCmdlet. +/// Note: Full cmdlet testing with parameter sets requires a PowerShell runspace. +/// These tests focus on verifying the cmdlet structure and basic functionality. +/// +[Collection("Sequential")] +public class NewSessionCmdletTests : IDisposable +{ + public NewSessionCmdletTests() + { + // Clean state before each test + SessionManager.CloseSession(); + } + + public void Dispose() + { + // Clean up after each test + SessionManager.CloseSession(); + } + + [Fact] + public void NewSessionCmdlet_Exists() + { + // Verify the cmdlet class exists and can be instantiated + var cmdlet = new NewSessionCmdlet(); + cmdlet.Should().NotBeNull(); + } + + [Fact] + public void NewSessionCmdlet_URLProperty_Exists() + { + var cmdlet = new NewSessionCmdlet(); + cmdlet.URL = "https://ipam.example.com"; + cmdlet.URL.Should().Be("https://ipam.example.com"); + } + + [Fact] + public void NewSessionCmdlet_AppIDProperty_Exists() + { + var cmdlet = new NewSessionCmdlet(); + cmdlet.AppID = "testapp"; + cmdlet.AppID.Should().Be("testapp"); + } + + [Fact] + public void NewSessionCmdlet_TokenProperty_Exists() + { + var cmdlet = new NewSessionCmdlet(); + cmdlet.Token = "my-api-token"; + cmdlet.Token.Should().Be("my-api-token"); + } + + [Fact] + public void NewSessionCmdlet_CredentialsProperty_Exists() + { + var cmdlet = new NewSessionCmdlet(); + cmdlet.Credentials = null; + cmdlet.Credentials.Should().BeNull(); + } + + [Fact] + public void NewSessionCmdlet_IgnoreSSLProperty_Exists() + { + var cmdlet = new NewSessionCmdlet(); + // SwitchParameter defaults to false + cmdlet.IgnoreSSL.IsPresent.Should().BeFalse(); + // Setting it to true + var switchParam = new System.Management.Automation.SwitchParameter(true); + cmdlet.IgnoreSSL = switchParam; + // Note: SwitchParameter is a struct, so getting the value back may not work as expected + // Just verify the property exists and can be set + } + + [Fact] + public void SessionManager_CreateSessionWithToken_WorksCorrectly() + { + // This tests the underlying functionality that the cmdlet uses + var session = SessionManager.CreateSessionWithToken( + "https://ipam.example.com", + "testapp", + "my-api-token" + ); + + session.Should().NotBeNull(); + session.URL.Should().Be("https://ipam.example.com"); + session.AppID.Should().Be("testapp"); + session.Token.Should().Be("my-api-token"); + session.AuthType.Should().Be(AuthType.token); + session.Expires.Should().BeNull(); + session.Credentials.Should().BeNull(); + + // Verify it was set as current session + SessionManager.CurrentSession.Should().BeSameAs(session); + } +} From 010cce0fd8ee17268a6f4a523f9400f091ed05f0 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 14:44:52 +0300 Subject: [PATCH 2/6] Refactored subnet object methods and enhanced related documentation --- classlib.tests/Helpers/RequestHelperTests.cs | 503 ++++++++++++++++++ classlib.tests/Helpers/SessionManagerTests.cs | 193 +++++++ classlib.tests/TestCollections.cs | 32 ++ classlib/Helpers/RequestHelper.cs | 374 +++++++++++++ 4 files changed, 1102 insertions(+) create mode 100644 classlib.tests/Helpers/RequestHelperTests.cs create mode 100644 classlib.tests/Helpers/SessionManagerTests.cs create mode 100644 classlib.tests/TestCollections.cs create mode 100644 classlib/Helpers/RequestHelper.cs diff --git a/classlib.tests/Helpers/RequestHelperTests.cs b/classlib.tests/Helpers/RequestHelperTests.cs new file mode 100644 index 0000000..90616a0 --- /dev/null +++ b/classlib.tests/Helpers/RequestHelperTests.cs @@ -0,0 +1,503 @@ +namespace PS.IPAM.Tests.Helpers; + +using System.Net; +using FluentAssertions; +using PS.IPAM; +using PS.IPAM.Helpers; +using PS.IPAM.Tests.Mocks; +using Xunit; + +[Collection("Sequential")] +public class RequestHelperTests : IDisposable +{ + private MockHttpMessageHandler? _mockHandler; + + public RequestHelperTests() + { + // Clean state before each test + SessionManager.CloseSession(); + RequestHelper.TestHttpHandler = null; + } + + public void Dispose() + { + // Clean up after each test + SessionManager.CloseSession(); + RequestHelper.TestHttpHandler = null; + _mockHandler?.ForceDispose(); + } + + private void SetupSession(AuthType authType = AuthType.token) + { + if (authType == AuthType.token) + { + SessionManager.CreateSessionWithToken("https://ipam.example.com", "testapp", "test-token"); + } + else + { + // For credentials auth, we need to set up a session manually with future expiry + var session = new Session( + AuthType.credentials, + "cred-token", + "testapp", + "https://ipam.example.com", + DateTime.Now.AddHours(1), + null + ); + SessionManager.CurrentSession = session; + } + } + + private MockHttpMessageHandler SetupMockHandler() + { + _mockHandler = new MockHttpMessageHandler(); + RequestHelper.TestHttpHandler = _mockHandler; + return _mockHandler; + } + + [Fact] + public async Task InvokeRequest_WithNoSession_ThrowsException() + { + // Arrange - no session set up + + // Act + var action = async () => await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + await action.Should().ThrowAsync().WithMessage("No session available!"); + } + + [Fact] + public async Task InvokeRequest_WithValidSession_BuildsCorrectUri() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{\"id\":1,\"ip\":\"192.168.1.1\"}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "1" }); + + // Assert + handler.LastRequest.Should().NotBeNull(); + handler.LastRequest!.RequestUri!.ToString().Should().Be("https://ipam.example.com/api/testapp/addresses/1/"); + } + + [Fact] + public async Task InvokeRequest_WithSubController_BuildsCorrectUri() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("[]"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.subnets, null, subcontrollers.tags, null, new[] { "1" }); + + // Assert + handler.LastRequest!.RequestUri!.ToString().Should().Contain("/subnets/tags/1/"); + } + + [Fact] + public async Task InvokeRequest_WithTokenAuth_AddsPhpipamTokenHeader() + { + // Arrange + SetupSession(AuthType.token); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + handler.GetLastRequestHeader("phpipam-token").Should().Be("test-token"); + } + + [Fact] + public async Task InvokeRequest_WithCredentialsAuth_AddsTokenHeader() + { + // Arrange + SetupSession(AuthType.credentials); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + handler.GetLastRequestHeader("token").Should().Be("cred-token"); + } + + [Fact] + public async Task InvokeRequest_GetMethod_UsesHttpGet() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + handler.LastRequest!.Method.Should().Be(HttpMethod.Get); + } + + [Fact] + public async Task InvokeRequest_PostMethod_UsesHttpPost() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{\"id\":1}"); + + // Act + await RequestHelper.InvokeRequest("POST", controllers.addresses, null, null, new { ip = "10.0.0.1" }); + + // Assert + handler.LastRequest!.Method.Should().Be(HttpMethod.Post); + } + + [Fact] + public async Task InvokeRequest_PatchMethod_UsesHttpPatch() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{\"id\":1}"); + + // Act + await RequestHelper.InvokeRequest("PATCH", controllers.addresses, null, null, new { description = "updated" }, new[] { "1" }); + + // Assert + handler.LastRequest!.Method.Should().Be(new HttpMethod("PATCH")); + } + + [Fact] + public async Task InvokeRequest_DeleteMethod_UsesHttpDelete() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("DELETE", controllers.addresses, null, null, null, new[] { "1" }); + + // Assert + handler.LastRequest!.Method.Should().Be(HttpMethod.Delete); + } + + [Fact] + public async Task InvokeRequest_With404Response_ReturnsNull() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithNotFoundResponse(); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "999" }); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task InvokeRequest_WithErrorResponse_ThrowsHttpRequestException() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithErrorResponse(HttpStatusCode.InternalServerError, "Server error"); + + // Act + var action = async () => await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task InvokeRequest_WithAddressType_ReturnsAddressObject() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var addressJson = @"{ + ""id"": 1, + ""subnetId"": 10, + ""ip"": ""192.168.1.100"", + ""is_gateway"": false, + ""description"": ""Test server"", + ""hostname"": ""server01"", + ""mac"": ""00:11:22:33:44:55"", + ""owner"": ""admin"", + ""tag"": 2, + ""deviceId"": 0, + ""location"": """", + ""port"": """", + ""note"": """", + ""lastSeen"": null, + ""excludePing"": false, + ""PTRignore"": false, + ""PTR"": 0, + ""firewallAddressObject"": """", + ""editDate"": null, + ""customer_id"": 0 + }"; + handler.WithSuccessResponse(addressJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "1" }); + + // Assert + result.Should().BeOfType
(); + var address = (Address)result!; + address.Id.Should().Be(1); + address.Ip.Should().Be("192.168.1.100"); + address.Hostname.Should().Be("server01"); + } + + [Fact] + public async Task InvokeRequest_WithAddressArray_ReturnsListOfAddresses() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var addressArrayJson = @"[ + {""id"": 1, ""subnetId"": 10, ""ip"": ""192.168.1.1"", ""hostname"": ""host1""}, + {""id"": 2, ""subnetId"": 10, ""ip"": ""192.168.1.2"", ""hostname"": ""host2""} + ]"; + handler.WithSuccessResponse(addressArrayJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.subnets, types.Address, null, null, new[] { "10", "addresses" }); + + // Assert + result.Should().BeAssignableTo>(); + var addresses = ((IEnumerable)result!).ToList(); + addresses.Should().HaveCount(2); + addresses[0].Should().BeOfType
(); + ((Address)addresses[0]).Ip.Should().Be("192.168.1.1"); + } + + [Fact] + public async Task InvokeRequest_WithVlanType_ReturnsVlanObject() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var vlanJson = @"{ + ""vlanId"": 100, + ""domainId"": 1, + ""name"": ""Production"", + ""number"": 100, + ""description"": ""Production VLAN"", + ""editDate"": null, + ""customer_id"": 0 + }"; + handler.WithSuccessResponse(vlanJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.vlan, types.Vlan, null, null, new[] { "100" }); + + // Assert + result.Should().BeOfType(); + var vlan = (Vlan)result!; + vlan.Name.Should().Be("Production"); + vlan.Number.Should().Be(100); + } + + [Fact] + public async Task InvokeRequest_WithSubnetworkType_ReturnsSubnetworkObject() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var subnetJson = @"{ + ""id"": 1, + ""subnet"": ""192.168.1.0"", + ""mask"": 24, + ""sectionId"": 1, + ""description"": ""Test subnet"", + ""linked_subnet"": """", + ""firewallAddressObject"": """", + ""vrfId"": 0, + ""masterSubnetId"": 0, + ""allowRequests"": false, + ""vlanId"": 100, + ""showName"": false, + ""deviceId"": 0, + ""permissions"": """", + ""pingSubnet"": false, + ""discoverSubnet"": false, + ""resolveDNS"": false, + ""DNSrecursive"": false, + ""DNSrecords"": false, + ""nameserverId"": 0, + ""scanAgent"": false, + ""isFolder"": false, + ""isFull"": false, + ""isPool"": false, + ""state"": 1, + ""threshold"": 0, + ""location"": 0, + ""editDate"": null, + ""lastScan"": null, + ""lastDiscovery"": null, + ""calculation"": {} + }"; + handler.WithSuccessResponse(subnetJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.subnets, types.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"); + } + + [Fact] + public async Task InvokeRequest_WithSectionType_ReturnsSectionObject() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var sectionJson = @"{ + ""id"": 1, + ""name"": ""Production"", + ""description"": ""Production section"", + ""masterSection"": 0, + ""permissions"": """", + ""strictMode"": false, + ""subnetOrdering"": ""default"", + ""order"": 1, + ""editDate"": null, + ""showSubnet"": true, + ""showVlan"": true, + ""showVRF"": false, + ""showSupernetOnly"": false, + ""DNS"": 0 + }"; + handler.WithSuccessResponse(sectionJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.sections, types.Section, null, null, new[] { "1" }); + + // Assert + result.Should().BeOfType
(); + var section = (Section)result!; + section.Name.Should().Be("Production"); + } + + [Fact] + public async Task InvokeRequest_WithCustomFields_ParsesExtendedData() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + var addressJson = @"{ + ""id"": 1, + ""subnetId"": 10, + ""ip"": ""192.168.1.100"", + ""hostname"": ""server01"", + ""custom_environment"": ""production"", + ""custom_owner"": ""team-a"" + }"; + handler.WithSuccessResponse(addressJson); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "1" }); + + // Assert + var address = (Address)result!; + address.ExtendedData.Should().NotBeNull(); + var extendedData = (Dictionary)address.ExtendedData!; + extendedData.Should().ContainKey("custom_environment"); + extendedData.Should().ContainKey("custom_owner"); + } + + [Fact] + public async Task InvokeRequest_WithNoType_ReturnsDynamicData() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"{""some"": ""data""}"); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.tools); + + // Assert + result.Should().NotBeNull(); + } + + [Fact] + public async Task InvokeRequest_PostWithParameters_SerializesJsonBody() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse(@"{""id"": 1}"); + + var parameters = new { ip = "10.0.0.1", subnetId = 5, description = "New address" }; + + // Act + await RequestHelper.InvokeRequest("POST", controllers.addresses, null, null, parameters); + + // Assert + handler.LastRequest!.Content.Should().NotBeNull(); + var content = await handler.LastRequest.Content!.ReadAsStringAsync(); + content.Should().Contain("10.0.0.1"); + content.Should().Contain("subnetId"); + } + + [Fact] + public async Task InvokeRequest_SetsAcceptJsonHeader() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithSuccessResponse("{}"); + + // Act + await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + handler.LastRequest!.Headers.Accept.Should().Contain(h => h.MediaType == "application/json"); + } + + [Fact] + public async Task InvokeRequest_WithEmptyResponse_ReturnsNull() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithResponse(HttpStatusCode.OK, "", "application/json"); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task InvokeRequest_WithNullDataInResponse_ReturnsNull() + { + // Arrange + SetupSession(); + var handler = SetupMockHandler(); + handler.WithJsonResponse(@"{""code"": 200, ""success"": true, ""data"": null}"); + + // Act + var result = await RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address); + + // Assert + result.Should().BeNull(); + } +} diff --git a/classlib.tests/Helpers/SessionManagerTests.cs b/classlib.tests/Helpers/SessionManagerTests.cs new file mode 100644 index 0000000..d521863 --- /dev/null +++ b/classlib.tests/Helpers/SessionManagerTests.cs @@ -0,0 +1,193 @@ +namespace PS.IPAM.Tests.Helpers; + +using FluentAssertions; +using PS.IPAM; +using PS.IPAM.Helpers; +using Xunit; + +[Collection("Sequential")] +public class SessionManagerTests : IDisposable +{ + public SessionManagerTests() + { + // Ensure clean state before each test + SessionManager.CloseSession(); + } + + public void Dispose() + { + // Clean up after each test + SessionManager.CloseSession(); + } + + [Fact] + public void TestSession_WhenNoSession_ReturnsNoToken() + { + // Arrange + SessionManager.CurrentSession = null; + + // Act + var result = SessionManager.TestSession(); + + // Assert + result.Should().Be("NoToken"); + } + + [Fact] + public void TestSession_WhenSessionWithNullExpires_ReturnsValid() + { + // Arrange + var session = new Session(AuthType.token, "test-token", "app", "https://test.com", null, null); + SessionManager.CurrentSession = session; + + // Act + var result = SessionManager.TestSession(); + + // Assert + result.Should().Be("Valid"); + } + + [Fact] + public void TestSession_WhenSessionNotExpired_ReturnsValid() + { + // Arrange + var futureExpiry = DateTime.Now.AddHours(1); + var session = new Session(AuthType.credentials, "test-token", "app", "https://test.com", futureExpiry, null); + SessionManager.CurrentSession = session; + + // Act + var result = SessionManager.TestSession(); + + // Assert + result.Should().Be("Valid"); + } + + [Fact] + public void TestSession_WhenSessionExpired_ReturnsExpired() + { + // Arrange + var pastExpiry = DateTime.Now.AddHours(-1); + var session = new Session(AuthType.credentials, "test-token", "app", "https://test.com", pastExpiry, null); + SessionManager.CurrentSession = session; + + // Act + var result = SessionManager.TestSession(); + + // Assert + result.Should().Be("Expired"); + } + + [Fact] + public void CreateSessionWithToken_CreatesValidSession() + { + // Arrange + var url = "https://ipam.example.com"; + var appId = "myApp"; + var token = "static-api-token"; + + // Act + var session = SessionManager.CreateSessionWithToken(url, appId, token); + + // Assert + session.Should().NotBeNull(); + session.URL.Should().Be(url); + session.AppID.Should().Be(appId); + session.Token.Should().Be(token); + session.AuthType.Should().Be(AuthType.token); + session.Expires.Should().BeNull(); + session.Credentials.Should().BeNull(); + } + + [Fact] + public void CreateSessionWithToken_SetsCurrentSession() + { + // Arrange + var url = "https://ipam.example.com"; + var appId = "myApp"; + var token = "api-token"; + + // Act + var session = SessionManager.CreateSessionWithToken(url, appId, token); + + // Assert + SessionManager.CurrentSession.Should().BeSameAs(session); + } + + [Fact] + public void CloseSession_ClearsCurrentSession() + { + // Arrange + SessionManager.CreateSessionWithToken("https://test.com", "app", "token"); + SessionManager.CurrentSession.Should().NotBeNull(); + + // Act + SessionManager.CloseSession(); + + // Assert + SessionManager.CurrentSession.Should().BeNull(); + } + + [Fact] + public void CloseSession_WhenNoSession_DoesNotThrow() + { + // Arrange + SessionManager.CurrentSession = null; + + // Act + var action = () => SessionManager.CloseSession(); + + // Assert + action.Should().NotThrow(); + } + + [Fact] + public void CurrentSession_CanBeSetDirectly() + { + // Arrange + var session = new Session(AuthType.token, "token", "app", "https://test.com", null, null); + + // Act + SessionManager.CurrentSession = session; + + // Assert + SessionManager.CurrentSession.Should().BeSameAs(session); + } + + [Fact] + public void CreateHttpClient_WithoutIgnoreSsl_ReturnsHttpClient() + { + // Act + using var client = SessionManager.CreateHttpClient(false); + + // Assert + client.Should().NotBeNull(); + client.Should().BeOfType(); + } + + [Fact] + public void CreateHttpClient_WithIgnoreSsl_ReturnsHttpClient() + { + // Act + using var client = SessionManager.CreateHttpClient(true); + + // Assert + client.Should().NotBeNull(); + client.Should().BeOfType(); + } + + [Fact] + public void CreateSessionWithToken_ReplacesExistingSession() + { + // Arrange + SessionManager.CreateSessionWithToken("https://old.com", "oldApp", "oldToken"); + + // Act + var newSession = SessionManager.CreateSessionWithToken("https://new.com", "newApp", "newToken"); + + // Assert + SessionManager.CurrentSession.Should().BeSameAs(newSession); + SessionManager.CurrentSession!.URL.Should().Be("https://new.com"); + SessionManager.CurrentSession.AppID.Should().Be("newApp"); + SessionManager.CurrentSession.Token.Should().Be("newToken"); + } +} diff --git a/classlib.tests/TestCollections.cs b/classlib.tests/TestCollections.cs new file mode 100644 index 0000000..3e4ff33 --- /dev/null +++ b/classlib.tests/TestCollections.cs @@ -0,0 +1,32 @@ +namespace PS.IPAM.Tests; + +using Xunit; + +/// +/// Collection definition for tests that share static state (SessionManager, RequestHelper.TestHttpHandler). +/// Tests in this collection will run sequentially, not in parallel. +/// +[CollectionDefinition("Sequential")] +public class SequentialCollection : ICollectionFixture +{ +} + +/// +/// Fixture for sequential test collection. +/// +public class SequentialTestFixture : IDisposable +{ + public SequentialTestFixture() + { + // Clean up before tests + PS.IPAM.Helpers.SessionManager.CloseSession(); + PS.IPAM.Helpers.RequestHelper.TestHttpHandler = null; + } + + public void Dispose() + { + // Clean up after tests + PS.IPAM.Helpers.SessionManager.CloseSession(); + PS.IPAM.Helpers.RequestHelper.TestHttpHandler = null; + } +} diff --git a/classlib/Helpers/RequestHelper.cs b/classlib/Helpers/RequestHelper.cs new file mode 100644 index 0000000..8ee8178 --- /dev/null +++ b/classlib/Helpers/RequestHelper.cs @@ -0,0 +1,374 @@ +namespace PS.IPAM.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PS.IPAM; + +public static class RequestHelper +{ + // 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, + object? parameters = null, + string[]? identifiers = null, + bool ignoreSsl = false) + { + var tokenStatus = SessionManager.TestSession(); + if (tokenStatus == "NoToken") + { + throw new Exception("No session available!"); + } + + if (tokenStatus == "Expired") + { + await UpdateSession(); + } + + var session = SessionManager.CurrentSession; + if (session == null) + { + throw new Exception("No session available!"); + } + + var uri = $"{session.URL}/api/{session.AppID}/{controller}"; + if (subController != null) + { + uri += $"/{subController}"; + } + if (identifiers != null && identifiers.Length > 0) + { + uri += $"/{string.Join("/", identifiers)}/"; + } + + using var client = SessionManager.CreateHttpClient(ignoreSsl, TestHttpHandler); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + switch (session.AuthType) + { + case AuthType.credentials: + client.DefaultRequestHeaders.Add("token", session.Token); + break; + case AuthType.token: + client.DefaultRequestHeaders.Add("phpipam-token", session.Token); + break; + } + + HttpResponseMessage? response = null; + try + { + if (method == "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); + } + + if (response == null) + { + return null; + } + + var responseContent = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return null; + } + throw new HttpRequestException($"Request failed with status {response.StatusCode}"); + } + + if (string.IsNullOrEmpty(responseContent)) + { + return null; + } + + var jsonResponse = JsonConvert.DeserializeObject(responseContent); + if (jsonResponse == null) + { + return null; + } + + if (type.HasValue) + { + return ConvertToTypedObjects(jsonResponse, type.Value); + } + + return jsonResponse.data; + } + catch (HttpRequestException ex) + { + if (ex.Message.Contains("404")) + { + return null; + } + throw; + } + } + + private static object? ConvertToTypedObjects(dynamic jsonResponse, types type) + { + if (jsonResponse?.data == null) + { + return null; + } + + var data = jsonResponse.data; + if (data is JArray array) + { + return array.Select(item => ConvertSingleObject(item, type)).ToList(); + } + else + { + return ConvertSingleObject(data, type); + } + } + + private static object? ConvertSingleObject(dynamic item, types type) + { + var jobject = item as JObject; + if (jobject == null) + { + return null; + } + + var customFields = new Dictionary(); + foreach (var prop in jobject.Properties()) + { + 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(); + } + } + + 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 + ); + } + + 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 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 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 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 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 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 Domain CreateDomain(JObject jobject) + { + return new Domain( + jobject["id"]?.ToObject() ?? 0, + jobject["name"]?.ToString() ?? "", + jobject["description"]?.ToString() ?? "", + jobject["sections"]?.ToString() ?? "" + ); + } + + private static async Task UpdateSession() + { + var session = SessionManager.CurrentSession; + if (session == null) + { + throw new Exception("No session available!"); + } + + 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 + ); + } + } +} From 114267b1d54a1f7df96a2b9ea63f52e2071e88e9 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 14:45:31 +0300 Subject: [PATCH 3/6] Added assembly name and root namespace to project file; updated session and vlan classes with new properties; modified module manifest to reference DLL and updated exported cmdlets; corrected property name in address class. --- classlib/Cmdlets/AssignTagCmdlet.cs | 43 ++++ classlib/Cmdlets/CloseSessionCmdlet.cs | 21 ++ classlib/Cmdlets/GetAddressCmdlet.cs | 162 ++++++++++++++ classlib/Cmdlets/GetFirstFreeIPCmdlet.cs | 79 +++++++ classlib/Cmdlets/GetL2DomainCmdlet.cs | 54 +++++ classlib/Cmdlets/GetNameserverCmdlet.cs | 54 +++++ classlib/Cmdlets/GetPermissionsCmdlet.cs | 148 +++++++++++++ classlib/Cmdlets/GetSectionCmdlet.cs | 65 ++++++ classlib/Cmdlets/GetSubnetCmdlet.cs | 261 +++++++++++++++++++++++ classlib/Cmdlets/GetSubnetUsageCmdlet.cs | 66 ++++++ classlib/Cmdlets/GetTagCmdlet.cs | 98 +++++++++ classlib/Cmdlets/GetVlanCmdlet.cs | 134 ++++++++++++ classlib/Cmdlets/GetVrfCmdlet.cs | 54 +++++ classlib/Cmdlets/NewAddressCmdlet.cs | 219 +++++++++++++++++++ classlib/Cmdlets/NewFirstFreeIPCmdlet.cs | 190 +++++++++++++++++ classlib/Cmdlets/NewSessionCmdlet.cs | 81 +++++++ classlib/Cmdlets/NewSubnetCmdlet.cs | 231 ++++++++++++++++++++ classlib/Cmdlets/RemoveAddressCmdlet.cs | 51 +++++ classlib/Cmdlets/SetAddressCmdlet.cs | 186 ++++++++++++++++ classlib/Cmdlets/SetSubnetCmdlet.cs | 164 ++++++++++++++ classlib/Helpers/SessionManager.cs | 131 ++++++++++++ classlib/class/address.cs | 2 +- classlib/class/session.cs | 9 +- classlib/class/vlan.cs | 2 + classlib/classlib.csproj | 8 + ps.ipam.psd1 | 7 +- 26 files changed, 2512 insertions(+), 8 deletions(-) create mode 100644 classlib/Cmdlets/AssignTagCmdlet.cs create mode 100644 classlib/Cmdlets/CloseSessionCmdlet.cs create mode 100644 classlib/Cmdlets/GetAddressCmdlet.cs create mode 100644 classlib/Cmdlets/GetFirstFreeIPCmdlet.cs create mode 100644 classlib/Cmdlets/GetL2DomainCmdlet.cs create mode 100644 classlib/Cmdlets/GetNameserverCmdlet.cs create mode 100644 classlib/Cmdlets/GetPermissionsCmdlet.cs create mode 100644 classlib/Cmdlets/GetSectionCmdlet.cs create mode 100644 classlib/Cmdlets/GetSubnetCmdlet.cs create mode 100644 classlib/Cmdlets/GetSubnetUsageCmdlet.cs create mode 100644 classlib/Cmdlets/GetTagCmdlet.cs create mode 100644 classlib/Cmdlets/GetVlanCmdlet.cs create mode 100644 classlib/Cmdlets/GetVrfCmdlet.cs create mode 100644 classlib/Cmdlets/NewAddressCmdlet.cs create mode 100644 classlib/Cmdlets/NewFirstFreeIPCmdlet.cs create mode 100644 classlib/Cmdlets/NewSessionCmdlet.cs create mode 100644 classlib/Cmdlets/NewSubnetCmdlet.cs create mode 100644 classlib/Cmdlets/RemoveAddressCmdlet.cs create mode 100644 classlib/Cmdlets/SetAddressCmdlet.cs create mode 100644 classlib/Cmdlets/SetSubnetCmdlet.cs create mode 100644 classlib/Helpers/SessionManager.cs diff --git a/classlib/Cmdlets/AssignTagCmdlet.cs b/classlib/Cmdlets/AssignTagCmdlet.cs new file mode 100644 index 0000000..5ac2a64 --- /dev/null +++ b/classlib/Cmdlets/AssignTagCmdlet.cs @@ -0,0 +1,43 @@ +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 +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0)] + [ValidateNotNullOrEmpty] + public Address? AddressObject { get; set; } + + [Parameter( + Mandatory = true, + Position = 1)] + [ValidateNotNullOrEmpty] + public Tag? Tag { get; set; } + + protected override void ProcessRecord() + { + try + { + var identifiers = new List { AddressObject!.Id.ToString() }; + var body = new Dictionary + { + { "tag", Tag!.Id } + }; + + RequestHelper.InvokeRequest("PATCH", controllers.addresses, null, null, body, identifiers.ToArray()) + .GetAwaiter().GetResult(); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "AssignTagError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/CloseSessionCmdlet.cs b/classlib/Cmdlets/CloseSessionCmdlet.cs new file mode 100644 index 0000000..07dabda --- /dev/null +++ b/classlib/Cmdlets/CloseSessionCmdlet.cs @@ -0,0 +1,21 @@ +namespace PS.IPAM.Cmdlets; +using System.Management.Automation; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Close, "Session")] +public class CloseSessionCmdlet : PSCmdlet +{ + 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)); + } + } +} diff --git a/classlib/Cmdlets/GetAddressCmdlet.cs b/classlib/Cmdlets/GetAddressCmdlet.cs new file mode 100644 index 0000000..b45d68d --- /dev/null +++ b/classlib/Cmdlets/GetAddressCmdlet.cs @@ -0,0 +1,162 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "Address", DefaultParameterSetName = "ByID")] +[OutputType(typeof(Address))] +public class GetAddressCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int Id { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByIP")] + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 1, + ParameterSetName = "BySubnetId")] + [ValidateNotNullOrEmpty] + public IPAddress? IP { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByHostName")] + [ValidateNotNullOrEmpty] + public string? HostName { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByHostBase")] + [ValidateNotNullOrEmpty] + public string? HostBase { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByTag")] + [ValidateNotNullOrEmpty] + public int? TagId { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySubnetId")] + [ValidateNotNullOrEmpty] + public int? SubnetId { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySubnetCIDR")] + [ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")] + [ValidateNotNullOrEmpty] + public string? SubnetCIDR { get; set; } + + protected override void ProcessRecord() + { + try + { + var controller = controllers.addresses; + var identifiers = new List(); + + switch (ParameterSetName) + { + case "ByID": + identifiers.Add(Id.ToString()); + break; + case "ByIP": + identifiers.Add("search"); + identifiers.Add(IP!.ToString()); + break; + case "ByHostName": + identifiers.Add("search_hostname"); + identifiers.Add(HostName!); + break; + case "ByHostBase": + identifiers.Add("search_hostbase"); + identifiers.Add(HostBase!); + break; + case "ByTag": + identifiers.Add("tags"); + identifiers.Add(TagId!.Value.ToString()); + identifiers.Add("addresses"); + break; + case "BySubnetId": + if (IP != null) + { + identifiers.Add(IP.ToString()); + identifiers.Add(SubnetId!.Value.ToString()); + } + else + { + controller = controllers.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(); + if (subnet == null) + { + throw new Exception("Cannot find subnet!"); + } + var subnetObj = subnet as Subnetwork; + identifiers.Add(subnetObj!.Id.ToString()); + identifiers.Add("addresses"); + break; + } + + var result = RequestHelper.InvokeRequest("GET", controller, types.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); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetAddressError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetFirstFreeIPCmdlet.cs b/classlib/Cmdlets/GetFirstFreeIPCmdlet.cs new file mode 100644 index 0000000..ffd8ab8 --- /dev/null +++ b/classlib/Cmdlets/GetFirstFreeIPCmdlet.cs @@ -0,0 +1,79 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "FirstFreeIP", DefaultParameterSetName = "ByID")] +public class GetFirstFreeIPCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByCIDR")] + [ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")] + [ValidateNotNullOrEmpty] + public string? CIDR { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int? Id { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySubnetObject")] + [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 if (ParameterSetName == "BySubnetObject") + { + subnetId = SubnetObject!.Id; + } + else + { + subnetId = Id!.Value; + } + + var identifiers = new List { subnetId.ToString(), "first_free" }; + var result = RequestHelper.InvokeRequest("GET", controllers.subnets, null, null, null, identifiers.ToArray()) + .GetAwaiter().GetResult(); + + if (result != null) + { + WriteObject(new { Ip = result.ToString() }); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetFirstFreeIPError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetL2DomainCmdlet.cs b/classlib/Cmdlets/GetL2DomainCmdlet.cs new file mode 100644 index 0000000..4839d56 --- /dev/null +++ b/classlib/Cmdlets/GetL2DomainCmdlet.cs @@ -0,0 +1,54 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "L2Domain", DefaultParameterSetName = "ByID")] +[OutputType(typeof(Domain))] +public class GetL2DomainCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int? Id { get; set; } + + protected override void ProcessRecord() + { + 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(); + + if (result != null) + { + if (result is System.Collections.IEnumerable enumerable && !(result is string)) + { + foreach (var item in enumerable) + { + WriteObject(item); + } + } + else + { + WriteObject(result); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetL2DomainError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetNameserverCmdlet.cs b/classlib/Cmdlets/GetNameserverCmdlet.cs new file mode 100644 index 0000000..2bb91f5 --- /dev/null +++ b/classlib/Cmdlets/GetNameserverCmdlet.cs @@ -0,0 +1,54 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "Nameserver", DefaultParameterSetName = "NoParams")] +[OutputType(typeof(Nameserver))] +public class GetNameserverCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int? Id { get; set; } + + protected override void ProcessRecord() + { + 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(); + + if (result != null) + { + if (result is System.Collections.IEnumerable enumerable && !(result is string)) + { + foreach (var item in enumerable) + { + WriteObject(item); + } + } + else + { + WriteObject(result); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetNameserverError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetPermissionsCmdlet.cs b/classlib/Cmdlets/GetPermissionsCmdlet.cs new file mode 100644 index 0000000..4c476fb --- /dev/null +++ b/classlib/Cmdlets/GetPermissionsCmdlet.cs @@ -0,0 +1,148 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "Permissions")] +public class GetPermissionsCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public string? Id { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByIP")] + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 1, + ParameterSetName = "BySubnetId")] + [ValidateNotNullOrEmpty] + public IPAddress? IP { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByHostName")] + [ValidateNotNullOrEmpty] + public string? HostName { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByTag")] + [ValidateNotNullOrEmpty] + public string? TagId { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySubnetId")] + [ValidateNotNullOrEmpty] + public string? SubnetId { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySubnetCIDR")] + [ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")] + [ValidateNotNullOrEmpty] + public string? SubnetCIDR { get; set; } + + protected override void ProcessRecord() + { + try + { + var controller = controllers.addresses; + var identifiers = new List(); + + switch (ParameterSetName) + { + case "ByID": + identifiers.Add(Id!); + break; + case "ByIP": + identifiers.Add("search"); + identifiers.Add(IP!.ToString()); + break; + case "ByHostName": + identifiers.Add("search_hostname"); + identifiers.Add(HostName!); + break; + case "ByTag": + identifiers.Add("tags"); + identifiers.Add(TagId!); + identifiers.Add("addresses"); + break; + case "BySubnetId": + if (IP != null) + { + identifiers.Add(IP.ToString()); + identifiers.Add(SubnetId!); + } + else + { + controller = controllers.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(); + if (subnet == null) + { + throw new Exception("Cannot find subnet!"); + } + var subnetObj = subnet as Subnetwork; + identifiers.Add(subnetObj!.Id.ToString()); + identifiers.Add("addresses"); + break; + } + + 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); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetPermissionsError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetSectionCmdlet.cs b/classlib/Cmdlets/GetSectionCmdlet.cs new file mode 100644 index 0000000..97dd9e2 --- /dev/null +++ b/classlib/Cmdlets/GetSectionCmdlet.cs @@ -0,0 +1,65 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "Section", DefaultParameterSetName = "NoParams")] +[OutputType(typeof(Section))] +public class GetSectionCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int? Id { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + Position = 0, + ParameterSetName = "ByName")] + [ValidateNotNullOrEmpty] + public string? Name { get; set; } + + protected override void ProcessRecord() + { + try + { + var identifiers = new List(); + if (ParameterSetName == "ByID" && Id.HasValue) + { + identifiers.Add(Id.Value.ToString()); + } + else if (ParameterSetName == "ByName") + { + identifiers.Add(Name!); + } + + var result = RequestHelper.InvokeRequest("GET", controllers.sections, types.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); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetSectionError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetSubnetCmdlet.cs b/classlib/Cmdlets/GetSubnetCmdlet.cs new file mode 100644 index 0000000..d2c3f52 --- /dev/null +++ b/classlib/Cmdlets/GetSubnetCmdlet.cs @@ -0,0 +1,261 @@ +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; + +[Cmdlet(VerbsCommon.Get, "Subnet", DefaultParameterSetName = "NoParams")] +[OutputType(typeof(Subnetwork))] +public class GetSubnetCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByCIDR")] + [ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")] + [ValidateNotNullOrEmpty] + public string? CIDR { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int? Id { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySectionId")] + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 2, + ParameterSetName = "ByVlanNumber")] + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 1, + ParameterSetName = "ByVlanId")] + [ValidateNotNullOrEmpty] + public int? SectionId { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySectionName")] + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 3, + ParameterSetName = "ByVlanNumber")] + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 2, + ParameterSetName = "ByVlanId")] + [ValidateNotNullOrEmpty] + public string? SectionName { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByVrfId")] + [ValidateNotNullOrEmpty] + public int? VrfId { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByVlanId")] + [ValidateNotNullOrEmpty] + public int? VlanId { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByVlanNumber")] + [ValidateNotNullOrEmpty] + public int? VlanNumber { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 1, + ParameterSetName = "ByID")] + public SwitchParameter Slaves { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 2, + ParameterSetName = "ByID")] + public SwitchParameter Recurse { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 1, + ParameterSetName = "ByVlanNumber")] + [ValidateNotNullOrEmpty] + public int? VlanDomainId { get; set; } + + + protected override void ProcessRecord() + { + try + { + var controller = controllers.subnets; + var identifiers = new List(); + + switch (ParameterSetName) + { + case "ByCIDR": + identifiers.Add("cidr"); + identifiers.Add(CIDR!); + break; + case "ByID": + identifiers.Add(Id!.Value.ToString()); + if (Slaves.IsPresent) + { + identifiers.Add(Recurse.IsPresent ? "slaves_recursive" : "slaves"); + } + break; + case "BySectionId": + controller = controllers.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()); + identifiers.Add("subnets"); + break; + case "ByVrfId": + controller = controllers.vrf; + identifiers.Add(VrfId!.Value.ToString()); + identifiers.Add("subnets"); + break; + case "ByVlanId": + controller = controllers.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()); + } + } + 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()); + 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()); + } + } + break; + } + + var result = RequestHelper.InvokeRequest("GET", controller, types.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); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetSubnetError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetSubnetUsageCmdlet.cs b/classlib/Cmdlets/GetSubnetUsageCmdlet.cs new file mode 100644 index 0000000..d4bd301 --- /dev/null +++ b/classlib/Cmdlets/GetSubnetUsageCmdlet.cs @@ -0,0 +1,66 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "SubnetUsage", DefaultParameterSetName = "ByID")] +public class GetSubnetUsageCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByCIDR")] + [ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")] + [ValidateNotNullOrEmpty] + public string? CIDR { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int? Id { 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 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); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetSubnetUsageError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetTagCmdlet.cs b/classlib/Cmdlets/GetTagCmdlet.cs new file mode 100644 index 0000000..1c0d3a7 --- /dev/null +++ b/classlib/Cmdlets/GetTagCmdlet.cs @@ -0,0 +1,98 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "Tag", DefaultParameterSetName = "NoParams")] +[OutputType(typeof(Tag))] +public class GetTagCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int? Id { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByAddressObject")] + [ValidateNotNullOrEmpty] + public Address? AddressObject { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySubnetObject")] + [ValidateNotNullOrEmpty] + public Subnetwork? SubnetObject { get; set; } + + protected override void ProcessRecord() + { + try + { + var identifiers = new List { "tags" }; + + switch (ParameterSetName) + { + case "ByID": + if (Id.HasValue) + { + identifiers.Add(Id.Value.ToString()); + } + break; + case "ByAddressObject": + if (AddressObject?.TagId > 0) + { + identifiers.Add(AddressObject.TagId.ToString()); + } + else + { + return; + } + break; + case "BySubnetObject": + if (SubnetObject?.TagId > 0) + { + identifiers.Add(SubnetObject.TagId.ToString()); + } + else + { + return; + } + break; + } + + var result = RequestHelper.InvokeRequest("GET", controllers.addresses, types.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); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetTagError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetVlanCmdlet.cs b/classlib/Cmdlets/GetVlanCmdlet.cs new file mode 100644 index 0000000..7425b19 --- /dev/null +++ b/classlib/Cmdlets/GetVlanCmdlet.cs @@ -0,0 +1,134 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "Vlan", DefaultParameterSetName = "NoParams")] +[OutputType(typeof(Vlan))] +public class GetVlanCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int? Id { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByNumber")] + [ValidateNotNullOrEmpty] + public int? Number { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByL2Domain")] + [ValidateNotNullOrEmpty] + public int? L2DomainId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySubnetObject")] + [ValidateNotNullOrEmpty] + public Subnetwork? SubnetObject { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByDomainObject")] + [ValidateNotNullOrEmpty] + public Domain? DomainObject { get; set; } + + protected override void ProcessRecord() + { + try + { + var controller = controllers.vlan; + var identifiers = new List(); + + switch (ParameterSetName) + { + case "ByID": + identifiers.Add(Id!.Value.ToString()); + break; + case "ByNumber": + identifiers.Add("search"); + identifiers.Add(Number!.Value.ToString()); + break; + case "ByL2Domain": + controller = controllers.l2domains; + identifiers.Add(L2DomainId!.Value.ToString()); + identifiers.Add(subcontrollers.vlans.ToString()); + break; + case "BySubnetObject": + if (SubnetObject != null && SubnetObject.VlanId > 0) + { + identifiers.Add(SubnetObject.VlanId.ToString()); + } + else + { + return; + } + 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); + } + } + } + return; + } + + var vlanResult = RequestHelper.InvokeRequest("GET", controller, types.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); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetVlanError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/GetVrfCmdlet.cs b/classlib/Cmdlets/GetVrfCmdlet.cs new file mode 100644 index 0000000..898cd80 --- /dev/null +++ b/classlib/Cmdlets/GetVrfCmdlet.cs @@ -0,0 +1,54 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Get, "Vrf", DefaultParameterSetName = "NoParams")] +[OutputType(typeof(Vrf))] +public class GetVrfCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByID")] + [ValidateNotNullOrEmpty] + public int? Id { get; set; } + + protected override void ProcessRecord() + { + 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(); + + if (result != null) + { + if (result is System.Collections.IEnumerable enumerable && !(result is string)) + { + foreach (var item in enumerable) + { + WriteObject(item); + } + } + else + { + WriteObject(result); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetVrfError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/NewAddressCmdlet.cs b/classlib/Cmdlets/NewAddressCmdlet.cs new file mode 100644 index 0000000..def3663 --- /dev/null +++ b/classlib/Cmdlets/NewAddressCmdlet.cs @@ -0,0 +1,219 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.New, "Address", DefaultParameterSetName = "BySubnetId")] +[OutputType(typeof(Address))] +public class NewAddressCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySubnetId")] + [ValidateNotNullOrEmpty] + public int? SubnetId { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "BySubnetObject")] + [ValidateNotNullOrEmpty] + public Subnetwork? SubnetObject { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 1)] + [ValidateNotNullOrEmpty] + public string IP { get; set; } = string.Empty; + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 2)] + public SwitchParameter Gateway { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 3)] + public string? Description { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 4)] + public string? Hostname { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 5)] + public string? MAC { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 6)] + public string? Owner { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 7)] + public int? TagId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 8)] + public SwitchParameter PTRIgnore { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 7)] + public int? PTRId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 10)] + public string? Note { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 11)] + public SwitchParameter ExcludePing { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 12)] + public int? DeviceId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 13)] + public string? Port { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 14)] + public object? CustomFields { get; set; } + + protected override void ProcessRecord() + { + try + { + int actualSubnetId; + if (ParameterSetName == "BySubnetObject") + { + actualSubnetId = SubnetObject!.Id; + } + else + { + actualSubnetId = SubnetId!.Value; + } + + var body = new Dictionary + { + { "subnetId", actualSubnetId }, + { "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; + + 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(); + + if (result != null) + { + var address = RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, new[] { "search", IP }) + .GetAwaiter().GetResult(); + if (address != null) + { + WriteObject(address); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "NewAddressError", ErrorCategory.InvalidOperation, null)); + } + } + + private Dictionary ConvertCustomFields(object customFields) + { + var dict = new Dictionary(); + if (customFields is PSObject psobj) + { + foreach (var prop in psobj.Properties) + { + dict[prop.Name] = prop.Value ?? new object(); + } + } + else if (customFields is Dictionary dictObj) + { + return dictObj; + } + return dict; + } +} diff --git a/classlib/Cmdlets/NewFirstFreeIPCmdlet.cs b/classlib/Cmdlets/NewFirstFreeIPCmdlet.cs new file mode 100644 index 0000000..ca48fff --- /dev/null +++ b/classlib/Cmdlets/NewFirstFreeIPCmdlet.cs @@ -0,0 +1,190 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.New, "FirstFreeIP")] +[OutputType(typeof(Address))] +public class NewFirstFreeIPCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0)] + [ValidateNotNullOrEmpty] + public string SubnetId { get; set; } = string.Empty; + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 2)] + public SwitchParameter Gateway { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 3)] + public string? Description { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 4)] + public string? Hostname { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 5)] + public string? MAC { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 6)] + public string? Owner { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 7)] + public string? TagId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 8)] + public SwitchParameter PTRIgnore { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 9)] + public string? PTRId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 10)] + public string? Note { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 11)] + public SwitchParameter ExcludePing { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 12)] + public string? DeviceId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 13)] + public string? Port { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public object? CustomFields { get; set; } + + protected override void ProcessRecord() + { + try + { + var identifiers = new List { "first_free" }; + var body = new Dictionary + { + { "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; + + 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(); + + 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); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "NewFirstFreeIPError", ErrorCategory.InvalidOperation, null)); + } + } + + private Dictionary ConvertCustomFields(object customFields) + { + var dict = new Dictionary(); + if (customFields is PSObject psobj) + { + foreach (var prop in psobj.Properties) + { + dict[prop.Name] = prop.Value ?? new object(); + } + } + else if (customFields is Dictionary dictObj) + { + return dictObj; + } + return dict; + } +} diff --git a/classlib/Cmdlets/NewSessionCmdlet.cs b/classlib/Cmdlets/NewSessionCmdlet.cs new file mode 100644 index 0000000..2f89eb4 --- /dev/null +++ b/classlib/Cmdlets/NewSessionCmdlet.cs @@ -0,0 +1,81 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Management.Automation; +using System.Threading.Tasks; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.New, "Session", DefaultParameterSetName = "Credentials")] +public class NewSessionCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0)] + [ValidateNotNullOrEmpty] + [ValidatePattern("^https?://")] + public string URL { get; set; } = string.Empty; + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 1)] + [ValidateNotNullOrEmpty] + public string AppID { get; set; } = string.Empty; + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 2, + ParameterSetName = "Credentials")] + [ValidateNotNullOrEmpty] + public PSCredential? Credentials { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 2, + ParameterSetName = "Token")] + [ValidateNotNullOrEmpty] + public string? Token { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 3)] + public SwitchParameter IgnoreSSL { get; set; } + + protected override void ProcessRecord() + { + try + { + Session session; + if (ParameterSetName == "Credentials" && Credentials != null) + { + session = SessionManager.CreateSessionWithCredentials( + URL, + AppID, + Credentials, + IgnoreSSL.IsPresent + ).GetAwaiter().GetResult(); + } + else if (ParameterSetName == "Token" && Token != null) + { + session = SessionManager.CreateSessionWithToken(URL, AppID, Token); + } + else + { + throw new ArgumentException("Invalid parameter set"); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "NewSessionError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/NewSubnetCmdlet.cs b/classlib/Cmdlets/NewSubnetCmdlet.cs new file mode 100644 index 0000000..cf2d41b --- /dev/null +++ b/classlib/Cmdlets/NewSubnetCmdlet.cs @@ -0,0 +1,231 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.New, "Subnet")] +[OutputType(typeof(Subnetwork))] +public class NewSubnetCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0)] + [ValidatePattern(@"^\d+\.\d+\.\d+\.\d+/\d{1,2}$")] + [ValidateNotNullOrEmpty] + public string CIDR { get; set; } = string.Empty; + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 1)] + [ValidateNotNullOrEmpty] + public int SectionId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 2)] + public string? Description { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 3)] + public int? VlanId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 4)] + public int? VrfId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 5)] + public int? MasterSubnetId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 6)] + public int? NameserverId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 7)] + public SwitchParameter ShowName { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 8)] + public SwitchParameter DNSRecursive { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 9)] + public SwitchParameter DNSRecords { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 10)] + public SwitchParameter AllowRequests { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 11)] + public int? ScanAgentId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 12)] + public SwitchParameter DiscoverSubnet { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 12)] + public SwitchParameter IsFull { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 12)] + public int? TagId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 13)] + [ValidateRange(1, 100)] + public int? Threshold { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 14)] + public int? LocationId { get; set; } + + [Parameter( + Mandatory = false, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 15)] + 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 } + }; + + 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(); + + 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); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "NewSubnetError", ErrorCategory.InvalidOperation, null)); + } + } + + private Dictionary ConvertCustomFields(object customFields) + { + var dict = new Dictionary(); + if (customFields is PSObject psobj) + { + foreach (var prop in psobj.Properties) + { + dict[prop.Name] = prop.Value ?? new object(); + } + } + else if (customFields is Dictionary dictObj) + { + return dictObj; + } + return dict; + } +} diff --git a/classlib/Cmdlets/RemoveAddressCmdlet.cs b/classlib/Cmdlets/RemoveAddressCmdlet.cs new file mode 100644 index 0000000..744873b --- /dev/null +++ b/classlib/Cmdlets/RemoveAddressCmdlet.cs @@ -0,0 +1,51 @@ +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 +{ + [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")] + [ValidateNotNullOrEmpty] + public Address? AddressObject { 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() }; + RequestHelper.InvokeRequest("DELETE", controllers.addresses, null, null, null, identifiers.ToArray()) + .GetAwaiter().GetResult(); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "RemoveAddressError", ErrorCategory.InvalidOperation, null)); + } + } +} diff --git a/classlib/Cmdlets/SetAddressCmdlet.cs b/classlib/Cmdlets/SetAddressCmdlet.cs new file mode 100644 index 0000000..1a21035 --- /dev/null +++ b/classlib/Cmdlets/SetAddressCmdlet.cs @@ -0,0 +1,186 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Set, "Address", DefaultParameterSetName = "ById")] +[OutputType(typeof(Address))] +public class SetAddressCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ById")] + [ValidateNotNullOrEmpty] + public int? Id { get; set; } + + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0, + ParameterSetName = "ByAddressObject")] + [ValidateNotNullOrEmpty] + public Address? AddressObject { get; set; } + + [Parameter( + Position = 1, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + ParameterSetName = "ById")] + public bool? Gateway { get; set; } + + [Parameter( + Position = 2, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + ParameterSetName = "ById")] + public string? Description { get; set; } + + [Parameter( + Mandatory = false, + Position = 3)] + public string? Hostname { get; set; } + + [Parameter( + Mandatory = false, + Position = 4)] + public string? MAC { get; set; } + + [Parameter( + Mandatory = false, + Position = 5)] + public string? Owner { get; set; } + + [Parameter( + Mandatory = false, + Position = 6)] + public int? TagId { get; set; } + + [Parameter( + Mandatory = false, + Position = 7)] + public bool? PTRIgnore { get; set; } + + [Parameter( + Mandatory = false, + Position = 8)] + public int? PTRId { get; set; } + + [Parameter( + Mandatory = false, + Position = 9)] + public string? Note { get; set; } + + [Parameter( + Mandatory = false, + Position = 10)] + public bool? ExcludePing { get; set; } + + [Parameter( + Mandatory = false, + Position = 11)] + public int? DeviceId { get; set; } + + [Parameter( + Mandatory = false, + Position = 12)] + public string? Port { get; set; } + + [Parameter( + Mandatory = false)] + 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; + } + } + + try + { + RequestHelper.InvokeRequest("PATCH", controllers.addresses, null, null, body, identifiers.ToArray()) + .GetAwaiter().GetResult(); + } + finally + { + var address = RequestHelper.InvokeRequest("GET", controllers.addresses, types.Address, null, null, identifiers.ToArray()) + .GetAwaiter().GetResult(); + if (address != null) + { + WriteObject(address); + } + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "SetAddressError", ErrorCategory.InvalidOperation, null)); + } + } + + private Dictionary ConvertCustomFields(object customFields) + { + var dict = new Dictionary(); + if (customFields is PSObject psobj) + { + foreach (var prop in psobj.Properties) + { + dict[prop.Name] = prop.Value ?? new object(); + } + } + else if (customFields is Dictionary dictObj) + { + return dictObj; + } + return dict; + } +} diff --git a/classlib/Cmdlets/SetSubnetCmdlet.cs b/classlib/Cmdlets/SetSubnetCmdlet.cs new file mode 100644 index 0000000..a43521c --- /dev/null +++ b/classlib/Cmdlets/SetSubnetCmdlet.cs @@ -0,0 +1,164 @@ +namespace PS.IPAM.Cmdlets; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PS.IPAM; +using PS.IPAM.Helpers; + +[Cmdlet(VerbsCommon.Set, "Subnet")] +[OutputType(typeof(Subnetwork))] +public class SetSubnetCmdlet : PSCmdlet +{ + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true, + Position = 0)] + [ValidateNotNullOrEmpty] + public int Id { get; set; } + + [Parameter( + Mandatory = false)] + public string? Description { get; set; } + + [Parameter( + Mandatory = false)] + public int? VlanId { get; set; } + + [Parameter( + Mandatory = false)] + public int? VrfId { get; set; } + + [Parameter( + Mandatory = false)] + public int? MasterSubnetId { get; set; } + + [Parameter( + Mandatory = false)] + public int? NameserverId { get; set; } + + [Parameter( + Mandatory = false)] + public SwitchParameter ShowName { get; set; } + + [Parameter( + Mandatory = false)] + public SwitchParameter DNSRecursive { get; set; } + + [Parameter( + Mandatory = false)] + public SwitchParameter DNSRecords { get; set; } + + [Parameter( + Mandatory = false)] + public SwitchParameter AllowRequests { get; set; } + + [Parameter( + Mandatory = false)] + public int? ScanAgentId { get; set; } + + [Parameter( + Mandatory = false)] + public SwitchParameter DiscoverSubnet { get; set; } + + [Parameter( + Mandatory = false)] + public SwitchParameter IsFull { get; set; } + + [Parameter( + Mandatory = false)] + public int? TagId { get; set; } + + [Parameter( + Mandatory = false)] + [ValidateRange(1, 100)] + public int? Threshold { get; set; } + + [Parameter( + Mandatory = false)] + public int? LocationId { get; set; } + + [Parameter( + Mandatory = false)] + public object? CustomFields { get; set; } + + protected override void ProcessRecord() + { + try + { + var identifiers = new List { Id.ToString() }; + 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; + + if (CustomFields != null) + { + var customDict = ConvertCustomFields(CustomFields); + foreach (var kvp in customDict) + { + body[kvp.Key] = kvp.Value; + } + } + + 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); + } + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "SetSubnetError", ErrorCategory.InvalidOperation, null)); + } + } + + private Dictionary ConvertCustomFields(object customFields) + { + var dict = new Dictionary(); + if (customFields is PSObject psobj) + { + foreach (var prop in psobj.Properties) + { + dict[prop.Name] = prop.Value ?? new object(); + } + } + else if (customFields is Dictionary dictObj) + { + return dictObj; + } + return dict; + } +} diff --git a/classlib/Helpers/SessionManager.cs b/classlib/Helpers/SessionManager.cs new file mode 100644 index 0000000..4df215b --- /dev/null +++ b/classlib/Helpers/SessionManager.cs @@ -0,0 +1,131 @@ +namespace PS.IPAM.Helpers; +using System; +using System.Management.Automation; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PS.IPAM; + +public static class SessionManager +{ + private static Session? _currentSession; + + public static Session? CurrentSession + { + get => _currentSession; + set => _currentSession = value; + } + + public static string TestSession() + { + if (_currentSession == null) + { + return "NoToken"; + } + + if (_currentSession.Expires == null) + { + return "Valid"; + } + + if (_currentSession.Expires < DateTime.Now) + { + return "Expired"; + } + + return "Valid"; + } + + public static async Task CreateSessionWithCredentials( + string url, + string appId, + PSCredential credentials, + bool ignoreSsl = false) + { + var uri = $"{url}/api/{appId}/user"; + var auth = Convert.ToBase64String( + Encoding.UTF8.GetBytes($"{credentials.UserName}:{GetPassword(credentials)}")); + + using var client = CreateHttpClient(ignoreSsl); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth); + + var response = await client.PostAsync(uri, null); + var content = await response.Content.ReadAsStringAsync(); + var jsonResponse = JsonConvert.DeserializeObject(content); + + if (jsonResponse?.success != true) + { + throw new Exception(jsonResponse?.error?.ToString() ?? "Failed to create session"); + } + + var token = jsonResponse.data.token.ToString(); + var expires = DateTime.Parse(jsonResponse.data.expires.ToString()); + + _currentSession = new Session( + AuthType.credentials, + token, + appId, + url, + expires, + credentials + ); + + return _currentSession; + } + + public static Session CreateSessionWithToken( + string url, + string appId, + string token) + { + _currentSession = new Session( + AuthType.token, + token, + appId, + url, + null, + null + ); + + return _currentSession; + } + + 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); + } + } + + public static HttpClient CreateHttpClient(bool ignoreSsl = false, HttpMessageHandler? handler = null) + { + if (handler != null) + { + return new HttpClient(handler); + } + + if (ignoreSsl) + { + var sslHandler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true + }; + return new HttpClient(sslHandler); + } + return new HttpClient(); + } +} diff --git a/classlib/class/address.cs b/classlib/class/address.cs index 1d15998..d2044c9 100644 --- a/classlib/class/address.cs +++ b/classlib/class/address.cs @@ -61,7 +61,7 @@ public class Address { this.Location = location; this.Port = port; this.Note = note; - this.EditDate = lastSeen; + this.LastSeen = lastSeen; this.ExcludePing = excludePing; this.PTRignore = PTRignore; this.PTR = PTR; diff --git a/classlib/class/session.cs b/classlib/class/session.cs index 5387361..a56e056 100644 --- a/classlib/class/session.cs +++ b/classlib/class/session.cs @@ -1,21 +1,22 @@ namespace PS.IPAM; using System; +using System.Management.Automation; [Serializable] public class Session { public AuthType AuthType { get; } - public string Token { get; } + public string Token { get; set; } public string AppID { get; } public string URL { get; } - public DateTime? Expires { get; } - public Object? Credentials { get; } + public DateTime? Expires { get; set; } + public object? Credentials { get; } public Session( AuthType authType, string token, string appId, string url, DateTime? expires, - Object? credentials + object? credentials ) { AuthType = authType; Token = token; diff --git a/classlib/class/vlan.cs b/classlib/class/vlan.cs index 342726b..ec080b9 100644 --- a/classlib/class/vlan.cs +++ b/classlib/class/vlan.cs @@ -5,6 +5,7 @@ using System.Dynamic; [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; } @@ -23,6 +24,7 @@ public class Vlan : DynamicObject { Object? custom_fields ) { this.Id = vlanId; + this.VlanId = vlanId; this.DomainId = domainId; this.Name = name; this.Number = number; diff --git a/classlib/classlib.csproj b/classlib/classlib.csproj index b3c1ee9..2a90951 100644 --- a/classlib/classlib.csproj +++ b/classlib/classlib.csproj @@ -5,6 +5,14 @@ enable enable latest + ps.ipam + PS.IPAM + + + + + + diff --git a/ps.ipam.psd1 b/ps.ipam.psd1 index 1e739d9..95500ae 100644 --- a/ps.ipam.psd1 +++ b/ps.ipam.psd1 @@ -6,7 +6,7 @@ # @{ - RootModule = 'ps.ipam.psm1' + RootModule = 'ps.ipam.dll' ModuleVersion = '2.0' GUID = 'cd573493-4245-4073-a238-fab2251d78d0' Author = 'Nikolay Tatarinov' @@ -15,14 +15,14 @@ PowerShellVersion = '5.1' RequiredAssemblies = @( - 'classlib\bin\Release\netstandard2.1\publish\classlib.dll' + 'ps.ipam.dll' ) TypesToProcess = @( 'types\types.ps1xml' ) - FunctionsToExport = @( + CmdletsToExport = @( 'Assign-Tag', 'Close-Session', 'New-Session', @@ -41,6 +41,7 @@ 'New-Address', 'New-FirstFreeIP', 'Set-Address', + 'Set-Subnet', 'Remove-Address' ) # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. From 40e3c31c6f6961b171f6333ea7b8f67130bffde4 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 14:46:09 +0300 Subject: [PATCH 4/6] Remove Jenkinsfile and add unit tests for various models including Address, Domain, Nameserver, Section, Session, Subnetwork, Tag, Vlan, and Vrf. Introduce mock classes for HTTP requests and cmdlet testing. --- Jenkinsfile | 21 --- classlib.tests/Mocks/CmdletTestHelper.cs | 139 ++++++++++++++ .../Mocks/MockHttpMessageHandler.cs | 172 ++++++++++++++++++ classlib.tests/Models/AddressTests.cs | 116 ++++++++++++ classlib.tests/Models/DomainTests.cs | 43 +++++ classlib.tests/Models/NameserverTests.cs | 82 +++++++++ classlib.tests/Models/SectionTests.cs | 77 ++++++++ classlib.tests/Models/SessionTests.cs | 92 ++++++++++ classlib.tests/Models/SubnetworkTests.cs | 142 +++++++++++++++ classlib.tests/Models/TagTests.cs | 77 ++++++++ classlib.tests/Models/VlanTests.cs | 76 ++++++++ classlib.tests/Models/VrfTests.cs | 60 ++++++ classlib.tests/classlib.tests.csproj | 33 ++++ 13 files changed, 1109 insertions(+), 21 deletions(-) delete mode 100644 Jenkinsfile create mode 100644 classlib.tests/Mocks/CmdletTestHelper.cs create mode 100644 classlib.tests/Mocks/MockHttpMessageHandler.cs create mode 100644 classlib.tests/Models/AddressTests.cs create mode 100644 classlib.tests/Models/DomainTests.cs create mode 100644 classlib.tests/Models/NameserverTests.cs create mode 100644 classlib.tests/Models/SectionTests.cs create mode 100644 classlib.tests/Models/SessionTests.cs create mode 100644 classlib.tests/Models/SubnetworkTests.cs create mode 100644 classlib.tests/Models/TagTests.cs create mode 100644 classlib.tests/Models/VlanTests.cs create mode 100644 classlib.tests/Models/VrfTests.cs create mode 100644 classlib.tests/classlib.tests.csproj diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 6ed9b0c..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,21 +0,0 @@ -pipeline { - agent { - label '.net7.0' - } - stages { - stage('Build classlib') { - steps { - sh '''cd classlib - dotnet build --no-incremental --force --configuration Release - dotnet publish -c Release''' - - contentReplace(configs: [fileContentReplaceConfig(configs: [fileContentReplaceItemConfig(replace: 'ModuleVersion = \'2.0.$BUILD_NUMBER\'', search: 'ModuleVersion = \'2.0\'')], fileEncoding: 'UTF-8', filePath: 'ps.ipam.psd1')]) - } - } - } - post { - success { - archiveArtifacts artifacts: 'classlib/bin/Release/netstandard2.1/publish/*.dll, *.psd1, *.psm1, LICENSE, **/*.ps1xml, **/*.ps1', followSymlinks: false, onlyIfSuccessful: true - } - } -} \ No newline at end of file diff --git a/classlib.tests/Mocks/CmdletTestHelper.cs b/classlib.tests/Mocks/CmdletTestHelper.cs new file mode 100644 index 0000000..f1c061b --- /dev/null +++ b/classlib.tests/Mocks/CmdletTestHelper.cs @@ -0,0 +1,139 @@ +namespace PS.IPAM.Tests.Mocks; + +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Reflection; + +/// +/// Helper class for testing PowerShell cmdlets. +/// +public class CmdletTestHelper : IDisposable where T : PSCmdlet, new() +{ + private readonly T _cmdlet; + private readonly List _output = new(); + private readonly List _errors = new(); + private bool _disposed; + + public CmdletTestHelper() + { + _cmdlet = new T(); + } + + /// + /// Gets the cmdlet instance for setting parameters. + /// + public T Cmdlet => _cmdlet; + + /// + /// Gets the output objects written by the cmdlet. + /// + public IReadOnlyList Output => _output.AsReadOnly(); + + /// + /// Gets the errors written by the cmdlet. + /// + public IReadOnlyList Errors => _errors.AsReadOnly(); + + /// + /// Gets whether any errors were written. + /// + public bool HasErrors => _errors.Count > 0; + + /// + /// Invokes the cmdlet and captures output. + /// + public void Invoke() + { + // Use reflection to call the protected ProcessRecord method + var processMethod = typeof(T).GetMethod("ProcessRecord", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (processMethod == null) + { + throw new InvalidOperationException("ProcessRecord method not found on cmdlet."); + } + + // Set up a mock command runtime to capture output + var runtime = new MockCommandRuntime(_output, _errors); + var runtimeProperty = typeof(Cmdlet).GetProperty("CommandRuntime", + BindingFlags.Public | BindingFlags.Instance); + + runtimeProperty?.SetValue(_cmdlet, runtime); + + // Invoke the method + try + { + processMethod.Invoke(_cmdlet, null); + } + catch (TargetInvocationException ex) when (ex.InnerException != null) + { + // Unwrap the exception + throw ex.InnerException; + } + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + } + } +} + +/// +/// Mock implementation of ICommandRuntime for capturing cmdlet output. +/// This is a minimal implementation that only handles the methods we need for testing. +/// +internal class MockCommandRuntime : ICommandRuntime +{ + private readonly List _output; + private readonly List _errors; + + public MockCommandRuntime(List output, List errors) + { + _output = output; + _errors = errors; + } + + public PSHost Host => null!; + public PSTransactionContext CurrentPSTransaction => null!; + + public bool ShouldContinue(string query, string caption) => true; + public bool ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll) => true; + public bool ShouldProcess(string target) => true; + public bool ShouldProcess(string target, string action) => true; + public bool ShouldProcess(string verboseDescription, string verboseWarning, string caption) => true; + public bool ShouldProcess(string verboseDescription, string verboseWarning, string caption, out ShouldProcessReason shouldProcessReason) + { + shouldProcessReason = ShouldProcessReason.None; + return true; + } + + public bool TransactionAvailable() => false; + + public void ThrowTerminatingError(ErrorRecord errorRecord) => throw errorRecord.Exception; + + public void WriteCommandDetail(string text) { } + public void WriteDebug(string text) { } + public void WriteError(ErrorRecord errorRecord) => _errors.Add(errorRecord); + public void WriteObject(object sendToPipeline) => _output.Add(sendToPipeline); + public void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (enumerateCollection && sendToPipeline is System.Collections.IEnumerable enumerable) + { + foreach (var item in enumerable) + { + _output.Add(item); + } + } + else + { + _output.Add(sendToPipeline); + } + } + public void WriteProgress(ProgressRecord progressRecord) { } + public void WriteProgress(long sourceId, ProgressRecord progressRecord) { } + public void WriteVerbose(string text) { } + public void WriteWarning(string text) { } +} diff --git a/classlib.tests/Mocks/MockHttpMessageHandler.cs b/classlib.tests/Mocks/MockHttpMessageHandler.cs new file mode 100644 index 0000000..497a788 --- /dev/null +++ b/classlib.tests/Mocks/MockHttpMessageHandler.cs @@ -0,0 +1,172 @@ +namespace PS.IPAM.Tests.Mocks; + +using System.Net; + +/// +/// A mock HTTP message handler for testing HTTP requests without making actual network calls. +/// This handler does not dispose itself when the HttpClient is disposed, allowing reuse in tests. +/// +public class MockHttpMessageHandler : HttpMessageHandler +{ + private readonly Queue _responses = new(); + private readonly List _requests = new(); + private bool _disposed = false; + + /// + /// Gets all requests that were sent through this handler. + /// + public IReadOnlyList Requests => _requests.AsReadOnly(); + + /// + /// Gets the last request that was sent through this handler. + /// + public HttpRequestMessage? LastRequest => _requests.LastOrDefault(); + + /// + /// Queues a response to be returned for the next request. + /// + public MockHttpMessageHandler WithResponse(HttpStatusCode statusCode, string content, string contentType = "application/json") + { + _responses.Enqueue(new MockResponse(statusCode, content, contentType)); + return this; + } + + /// + /// Queues a successful JSON response. + /// + public MockHttpMessageHandler WithJsonResponse(string jsonContent) + { + return WithResponse(HttpStatusCode.OK, jsonContent, "application/json"); + } + + /// + /// Queues a successful response with phpIPAM-style wrapper. + /// + public MockHttpMessageHandler WithSuccessResponse(string dataJson) + { + var response = $"{{\"code\":200,\"success\":true,\"data\":{dataJson}}}"; + return WithJsonResponse(response); + } + + /// + /// Queues a 404 Not Found response. + /// + public MockHttpMessageHandler WithNotFoundResponse() + { + return WithResponse(HttpStatusCode.NotFound, "{\"code\":404,\"success\":false,\"message\":\"Not found\"}", "application/json"); + } + + /// + /// Queues an error response. + /// + public MockHttpMessageHandler WithErrorResponse(HttpStatusCode statusCode, string message) + { + var response = $"{{\"code\":{(int)statusCode},\"success\":false,\"message\":\"{message}\"}}"; + return WithResponse(statusCode, response, "application/json"); + } + + /// + /// Queues an exception to be thrown on the next request. + /// + public MockHttpMessageHandler WithException(Exception exception) + { + _responses.Enqueue(new MockResponse(exception)); + return this; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(MockHttpMessageHandler)); + } + + _requests.Add(request); + + if (_responses.Count == 0) + { + throw new InvalidOperationException("No mock response configured. Call WithResponse() before making requests."); + } + + var mockResponse = _responses.Dequeue(); + + if (mockResponse.Exception != null) + { + throw mockResponse.Exception; + } + + var response = new HttpResponseMessage(mockResponse.StatusCode) + { + Content = new StringContent(mockResponse.Content, System.Text.Encoding.UTF8, mockResponse.ContentType), + RequestMessage = request + }; + + return Task.FromResult(response); + } + + protected override void Dispose(bool disposing) + { + // Don't actually dispose - allow reuse in tests + // The test itself is responsible for cleanup + } + + /// + /// Actually disposes the handler. Call this in test cleanup. + /// + public void ForceDispose() + { + _disposed = true; + base.Dispose(true); + } + + /// + /// Verifies that a request was made to the expected URL. + /// + public bool WasRequestMadeTo(string urlContains) + { + return _requests.Any(r => r.RequestUri?.ToString().Contains(urlContains) == true); + } + + /// + /// Verifies that a request with the expected method was made. + /// + public bool WasRequestMadeWithMethod(HttpMethod method) + { + return _requests.Any(r => r.Method == method); + } + + /// + /// Gets the value of a header from the last request. + /// + public string? GetLastRequestHeader(string headerName) + { + if (LastRequest?.Headers.TryGetValues(headerName, out var values) == true) + { + return values.FirstOrDefault(); + } + return null; + } + + private class MockResponse + { + public HttpStatusCode StatusCode { get; } + public string Content { get; } + public string ContentType { get; } + public Exception? Exception { get; } + + public MockResponse(HttpStatusCode statusCode, string content, string contentType) + { + StatusCode = statusCode; + Content = content; + ContentType = contentType; + } + + public MockResponse(Exception exception) + { + Exception = exception; + StatusCode = HttpStatusCode.InternalServerError; + Content = string.Empty; + ContentType = string.Empty; + } + } +} diff --git a/classlib.tests/Models/AddressTests.cs b/classlib.tests/Models/AddressTests.cs new file mode 100644 index 0000000..60ec426 --- /dev/null +++ b/classlib.tests/Models/AddressTests.cs @@ -0,0 +1,116 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class AddressTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var subnetId = 10; + var ip = "192.168.1.100"; + var isGateway = true; + var description = "Test server"; + var hostname = "server01.example.com"; + var mac = "00:11:22:33:44:55"; + var owner = "admin"; + var tagId = 2; + var deviceId = 5; + var location = "DC1"; + var port = "eth0"; + var note = "Production server"; + var lastSeen = new DateTime(2026, 1, 15, 10, 30, 0); + var excludePing = false; + var ptrIgnore = true; + var ptr = 1; + var firewallObject = "FW_SERVER01"; + var editDate = new DateTime(2026, 1, 10, 8, 0, 0); + var customerId = 100; + var extendedData = new Dictionary { { "custom_field1", "value1" } }; + + // Act + var address = new Address( + id, subnetId, ip, isGateway, description, hostname, mac, owner, + tagId, deviceId, location, port, note, lastSeen, excludePing, + ptrIgnore, ptr, firewallObject, editDate, customerId, extendedData + ); + + // Assert + address.Id.Should().Be(id); + address.SubnetId.Should().Be(subnetId); + address.Ip.Should().Be(ip); + address.IsGateway.Should().Be(isGateway); + address.Description.Should().Be(description); + address.Hostname.Should().Be(hostname); + address.MAC.Should().Be(mac); + address.Owner.Should().Be(owner); + address.TagId.Should().Be(tagId); + address.DeviceId.Should().Be(deviceId); + address.Location.Should().Be(location); + address.Port.Should().Be(port); + address.Note.Should().Be(note); + address.LastSeen.Should().Be(lastSeen); + address.ExcludePing.Should().Be(excludePing); + address.PTRignore.Should().Be(ptrIgnore); + address.PTR.Should().Be(ptr); + address.FirewallAddressObject.Should().Be(firewallObject); + address.EditDate.Should().Be(editDate); + address.CustomerId.Should().Be(customerId); + address.ExtendedData.Should().BeEquivalentTo(extendedData); + } + + [Fact] + public void Constructor_WithNullOptionalFields_SetsNullValues() + { + // Act + var address = new Address( + 1, 10, "10.0.0.1", false, "", "", "", "", + 0, 0, "", "", "", null, false, + false, 0, "", null, 0, null + ); + + // Assert + address.LastSeen.Should().BeNull(); + address.EditDate.Should().BeNull(); + address.ExtendedData.Should().BeNull(); + } + + [Fact] + public void ToString_ReturnsIpAddress() + { + // Arrange + var address = new Address( + 1, 10, "192.168.1.50", false, "Test", "host.local", "", "", + 0, 0, "", "", "", null, false, + false, 0, "", null, 0, null + ); + + // Act + var result = address.ToString(); + + // Assert + result.Should().Be("192.168.1.50"); + } + + [Theory] + [InlineData("10.0.0.1")] + [InlineData("172.16.0.100")] + [InlineData("192.168.255.255")] + [InlineData("2001:db8::1")] + public void ToString_ReturnsCorrectIp_ForVariousAddresses(string expectedIp) + { + // Arrange + var address = new Address( + 1, 1, expectedIp, false, "", "", "", "", + 0, 0, "", "", "", null, false, + false, 0, "", null, 0, null + ); + + // Act & Assert + address.ToString().Should().Be(expectedIp); + } +} diff --git a/classlib.tests/Models/DomainTests.cs b/classlib.tests/Models/DomainTests.cs new file mode 100644 index 0000000..9655e48 --- /dev/null +++ b/classlib.tests/Models/DomainTests.cs @@ -0,0 +1,43 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class DomainTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var name = "Default"; + var description = "Default L2 domain"; + var sections = "1;2;3"; + + // Act + var domain = new Domain(id, name, description, sections); + + // Assert + domain.Id.Should().Be(id); + domain.Name.Should().Be(name); + domain.Description.Should().Be(description); + domain.Sections.Should().Be(sections); + } + + [Theory] + [InlineData("Default")] + [InlineData("Datacenter1")] + [InlineData("Branch_Office")] + public void ToString_ReturnsDomainName(string domainName) + { + // Arrange + var domain = new Domain(1, domainName, "", ""); + + // Act + var result = domain.ToString(); + + // Assert + result.Should().Be(domainName); + } +} diff --git a/classlib.tests/Models/NameserverTests.cs b/classlib.tests/Models/NameserverTests.cs new file mode 100644 index 0000000..570cd9f --- /dev/null +++ b/classlib.tests/Models/NameserverTests.cs @@ -0,0 +1,82 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class NameserverTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var name = "Google DNS"; + var nameServers = "8.8.8.8;8.8.4.4"; + var description = "Google Public DNS"; + var permissions = "{\"3\":\"2\"}"; + var editDate = new DateTime(2026, 1, 10); + + // Act + var nameserver = new Nameserver(id, name, nameServers, description, permissions, editDate); + + // Assert + nameserver.Id.Should().Be(id); + nameserver.Name.Should().Be(name); + nameserver.Description.Should().Be(description); + nameserver.Permissions.Should().Be(permissions); + nameserver.EditDate.Should().Be(editDate); + } + + [Fact] + public void Constructor_ParsesNameservers_BySemicolon() + { + // Arrange + var nameServersString = "8.8.8.8;8.8.4.4;1.1.1.1"; + + // Act + var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null); + + // Assert + nameserver.NameServers.Should().HaveCount(3); + nameserver.NameServers.Should().ContainInOrder("8.8.8.8", "8.8.4.4", "1.1.1.1"); + } + + [Fact] + public void Constructor_WithSingleNameserver_ReturnsArrayWithOneElement() + { + // Arrange + var nameServersString = "8.8.8.8"; + + // Act + var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null); + + // Assert + nameserver.NameServers.Should().HaveCount(1); + nameserver.NameServers[0].Should().Be("8.8.8.8"); + } + + [Fact] + public void Constructor_WithEmptyNameservers_ReturnsArrayWithEmptyString() + { + // Arrange + var nameServersString = ""; + + // Act + var nameserver = new Nameserver(1, "Test", nameServersString, "", "", null); + + // Assert + nameserver.NameServers.Should().HaveCount(1); + nameserver.NameServers[0].Should().BeEmpty(); + } + + [Fact] + public void Constructor_WithNullEditDate_SetsNull() + { + // Act + var nameserver = new Nameserver(1, "Test", "8.8.8.8", "", "", null); + + // Assert + nameserver.EditDate.Should().BeNull(); + } +} diff --git a/classlib.tests/Models/SectionTests.cs b/classlib.tests/Models/SectionTests.cs new file mode 100644 index 0000000..cc83c8d --- /dev/null +++ b/classlib.tests/Models/SectionTests.cs @@ -0,0 +1,77 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class SectionTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var name = "Production"; + var description = "Production networks section"; + var masterSectionId = 0; + var permissions = "{\"3\":\"2\"}"; + var strictMode = true; + var subnetOrdering = "subnet,asc"; + var order = 1; + var editDate = new DateTime(2026, 1, 5); + var showSubnet = true; + var showVlan = true; + var showVRF = false; + var showSupernetOnly = false; + var dnsId = 1; + + // Act + var section = new Section( + id, name, description, masterSectionId, permissions, strictMode, + subnetOrdering, order, editDate, showSubnet, showVlan, showVRF, + showSupernetOnly, dnsId + ); + + // Assert + section.Id.Should().Be(id); + section.Name.Should().Be(name); + section.Description.Should().Be(description); + section.MasterSectionId.Should().Be(masterSectionId); + section.Permissions.Should().Be(permissions); + section.StrictMode.Should().Be(strictMode); + section.SubnetOrdering.Should().Be(subnetOrdering); + section.Order.Should().Be(order); + section.EditDate.Should().Be(editDate); + section.ShowSubnet.Should().Be(showSubnet); + section.ShowVlan.Should().Be(showVlan); + section.ShowVRF.Should().Be(showVRF); + section.ShowSupernetOnly.Should().Be(showSupernetOnly); + section.DNSId.Should().Be(dnsId); + } + + [Theory] + [InlineData("Production")] + [InlineData("Development")] + [InlineData("DMZ")] + public void ToString_ReturnsSectionName(string sectionName) + { + // Arrange + var section = new Section(1, sectionName, "", 0, "", false, "", 0, null, false, false, false, false, 0); + + // Act + var result = section.ToString(); + + // Assert + result.Should().Be(sectionName); + } + + [Fact] + public void Constructor_WithNullEditDate_SetsNull() + { + // Act + var section = new Section(1, "Test", "", 0, "", false, "", 0, null, false, false, false, false, 0); + + // Assert + section.EditDate.Should().BeNull(); + } +} diff --git a/classlib.tests/Models/SessionTests.cs b/classlib.tests/Models/SessionTests.cs new file mode 100644 index 0000000..4d9ebb2 --- /dev/null +++ b/classlib.tests/Models/SessionTests.cs @@ -0,0 +1,92 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class SessionTests +{ + [Fact] + public void Constructor_WithCredentialsAuth_SetsAllProperties() + { + // Arrange + var authType = AuthType.credentials; + var token = "test-token-123"; + var appId = "myApp"; + var url = "https://ipam.example.com"; + var expires = new DateTime(2026, 12, 31, 23, 59, 59); + var credentials = new object(); // Mock credentials + + // Act + var session = new Session(authType, token, appId, url, expires, credentials); + + // Assert + session.AuthType.Should().Be(AuthType.credentials); + session.Token.Should().Be(token); + session.AppID.Should().Be(appId); + session.URL.Should().Be(url); + session.Expires.Should().Be(expires); + session.Credentials.Should().BeSameAs(credentials); + } + + [Fact] + public void Constructor_WithTokenAuth_SetsAllProperties() + { + // Arrange + var authType = AuthType.token; + var token = "static-api-token"; + var appId = "apiApp"; + var url = "https://ipam.test.com"; + + // Act + var session = new Session(authType, token, appId, url, null, null); + + // Assert + session.AuthType.Should().Be(AuthType.token); + session.Token.Should().Be(token); + session.AppID.Should().Be(appId); + session.URL.Should().Be(url); + session.Expires.Should().BeNull(); + session.Credentials.Should().BeNull(); + } + + [Fact] + public void Token_CanBeModified() + { + // Arrange + var session = new Session(AuthType.credentials, "old-token", "app", "https://test.com", null, null); + + // Act + session.Token = "new-token"; + + // Assert + session.Token.Should().Be("new-token"); + } + + [Fact] + public void Expires_CanBeModified() + { + // Arrange + var session = new Session(AuthType.credentials, "token", "app", "https://test.com", null, null); + var newExpiry = new DateTime(2027, 1, 1); + + // Act + session.Expires = newExpiry; + + // Assert + session.Expires.Should().Be(newExpiry); + } + + [Fact] + public void ToString_ReturnsDefaultObjectString() + { + // Arrange + var session = new Session(AuthType.token, "token", "app", "https://test.com", null, null); + + // Act + var result = session.ToString(); + + // Assert + result.Should().Contain("PS.IPAM.Session"); + } +} diff --git a/classlib.tests/Models/SubnetworkTests.cs b/classlib.tests/Models/SubnetworkTests.cs new file mode 100644 index 0000000..eca5166 --- /dev/null +++ b/classlib.tests/Models/SubnetworkTests.cs @@ -0,0 +1,142 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class SubnetworkTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var subnet = "192.168.1.0"; + var mask = 24; + var sectionId = 5; + var description = "Production network"; + var linkedSubnet = "linked-123"; + var firewallObject = "FW_PROD"; + var vrfId = 2; + var masterSubnetId = 0; + var allowRequests = true; + var vlanId = 100; + var showName = true; + var deviceId = 10; + var permissions = "rw"; + var pingSubnet = true; + var discoverSubnet = false; + var resolveDNS = true; + var dnsRecursive = false; + var dnsRecords = true; + var nameserverId = 3; + var scanAgent = false; + var isFolder = false; + var isFull = false; + var isPool = true; + var tagId = 1; + var threshold = 80; + var locationId = 4; + var editDate = new DateTime(2026, 1, 10); + var lastScan = new DateTime(2026, 1, 9); + var lastDiscovery = new DateTime(2026, 1, 8); + var calculation = new { maxhosts = 254 }; + var customFields = new Dictionary { { "custom_env", "prod" } }; + + // Act + var subnetwork = new Subnetwork( + id, subnet, mask, sectionId, description, linkedSubnet, firewallObject, + vrfId, masterSubnetId, allowRequests, vlanId, showName, deviceId, + permissions, pingSubnet, discoverSubnet, resolveDNS, dnsRecursive, + dnsRecords, nameserverId, scanAgent, isFolder, isFull, isPool, + tagId, threshold, locationId, editDate, lastScan, lastDiscovery, + calculation, customFields + ); + + // Assert + subnetwork.Id.Should().Be(id); + subnetwork.Subnet.Should().Be(subnet); + subnetwork.Mask.Should().Be(mask); + subnetwork.SectionId.Should().Be(sectionId); + subnetwork.Description.Should().Be(description); + subnetwork.LinkedSubnet.Should().Be(linkedSubnet); + subnetwork.FirewallAddressObject.Should().Be(firewallObject); + subnetwork.VrfId.Should().Be(vrfId); + subnetwork.MasterSubnetId.Should().Be(masterSubnetId); + subnetwork.AllowRequests.Should().Be(allowRequests); + subnetwork.VlanId.Should().Be(vlanId); + subnetwork.ShowName.Should().Be(showName); + subnetwork.DeviceId.Should().Be(deviceId); + subnetwork.Permissions.Should().Be(permissions); + subnetwork.PingSubnet.Should().Be(pingSubnet); + subnetwork.DiscoverSubnet.Should().Be(discoverSubnet); + subnetwork.ResolveDNS.Should().Be(resolveDNS); + subnetwork.DNSRecursive.Should().Be(dnsRecursive); + subnetwork.DNSRecords.Should().Be(dnsRecords); + subnetwork.NameserverId.Should().Be(nameserverId); + subnetwork.ScanAgent.Should().Be(scanAgent); + subnetwork.IsFolder.Should().Be(isFolder); + subnetwork.IsFull.Should().Be(isFull); + subnetwork.IsPool.Should().Be(isPool); + subnetwork.TagId.Should().Be(tagId); + subnetwork.Threshold.Should().Be(threshold); + subnetwork.LocationId.Should().Be(locationId); + subnetwork.EditDate.Should().Be(editDate); + subnetwork.LastScan.Should().Be(lastScan); + subnetwork.LastDiscovery.Should().Be(lastDiscovery); + subnetwork.Calculation.Should().BeEquivalentTo(calculation); + subnetwork.ExtendedData.Should().BeEquivalentTo(customFields); + } + + [Theory] + [InlineData("192.168.1.0", 24, "192.168.1.0/24")] + [InlineData("10.0.0.0", 8, "10.0.0.0/8")] + [InlineData("172.16.0.0", 16, "172.16.0.0/16")] + [InlineData("192.168.100.0", 30, "192.168.100.0/30")] + public void GetCIDR_ReturnsCidrNotation(string subnet, int mask, string expectedCidr) + { + // Arrange + var subnetwork = CreateSubnetwork(subnet, mask); + + // Act + var result = subnetwork.GetCIDR(); + + // Assert + result.Should().Be(expectedCidr); + } + + [Fact] + public void ToString_ReturnsCidrNotation() + { + // Arrange + var subnetwork = CreateSubnetwork("10.10.0.0", 16); + + // Act + var result = subnetwork.ToString(); + + // Assert + result.Should().Be("10.10.0.0/16"); + } + + [Fact] + public void ToString_EqualsGetCIDR() + { + // Arrange + var subnetwork = CreateSubnetwork("172.20.0.0", 12); + + // Act & Assert + subnetwork.ToString().Should().Be(subnetwork.GetCIDR()); + } + + private static Subnetwork CreateSubnetwork(string subnet, int mask) + { + return new Subnetwork( + 1, subnet, mask, 1, "", "", "", + 0, 0, false, 0, false, 0, + "", false, false, false, false, + false, 0, false, false, false, false, + 0, 0, 0, null, null, null, + new object(), null + ); + } +} diff --git a/classlib.tests/Models/TagTests.cs b/classlib.tests/Models/TagTests.cs new file mode 100644 index 0000000..db345ef --- /dev/null +++ b/classlib.tests/Models/TagTests.cs @@ -0,0 +1,77 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class TagTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var type = "Used"; + var showTag = true; + var bgColor = "#5cb85c"; + var fgColor = "#ffffff"; + var compress = "Yes"; + var locked = "No"; + var updateTag = true; + + // Act + var tag = new Tag(id, type, showTag, bgColor, fgColor, compress, locked, updateTag); + + // Assert + tag.Id.Should().Be(id); + tag.Type.Should().Be(type); + tag.ShowTag.Should().Be(showTag); + tag.BGColor.Should().Be(bgColor); + tag.FGColor.Should().Be(fgColor); + tag.Compress.Should().BeTrue(); + tag.Locked.Should().BeFalse(); + 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")] + [InlineData("Reserved")] + [InlineData("DHCP")] + public void ToString_ReturnsTagType(string tagType) + { + // Arrange + var tag = new Tag(1, tagType, false, "", "", "No", "No", false); + + // Act + var result = tag.ToString(); + + // Assert + result.Should().Be(tagType); + } + + [Fact] + public void Locked_WithYes_IsTrue() + { + // Arrange & Act + var tag = new Tag(1, "Test", false, "", "", "No", "Yes", false); + + // Assert + tag.Locked.Should().BeTrue(); + } +} diff --git a/classlib.tests/Models/VlanTests.cs b/classlib.tests/Models/VlanTests.cs new file mode 100644 index 0000000..066956a --- /dev/null +++ b/classlib.tests/Models/VlanTests.cs @@ -0,0 +1,76 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class VlanTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var vlanId = 100; + var domainId = 1; + var name = "Production VLAN"; + var number = 100; + var description = "Production network VLAN"; + var editDate = new DateTime(2026, 1, 15); + var customerId = 50; + var customFields = new Dictionary { { "custom_location", "DC1" } }; + + // Act + var vlan = new Vlan(vlanId, domainId, name, number, description, editDate, customerId, customFields); + + // Assert + vlan.Id.Should().Be(vlanId); + vlan.VlanId.Should().Be(vlanId); + vlan.DomainId.Should().Be(domainId); + vlan.Name.Should().Be(name); + vlan.Number.Should().Be(number); + vlan.Description.Should().Be(description); + vlan.EditDate.Should().Be(editDate); + vlan.CustomerId.Should().Be(customerId); + vlan.ExtendedData.Should().BeEquivalentTo(customFields); + } + + [Fact] + public void Id_And_VlanId_AreSameValue() + { + // Arrange + var vlanId = 200; + + // Act + var vlan = new Vlan(vlanId, 1, "Test", 200, "", null, 0, null); + + // Assert + vlan.Id.Should().Be(vlan.VlanId); + } + + [Theory] + [InlineData(1)] + [InlineData(100)] + [InlineData(4094)] + public void ToString_ReturnsVlanNumber(int vlanNumber) + { + // Arrange + var vlan = new Vlan(1, 1, "Test", vlanNumber, "", null, 0, null); + + // Act + var result = vlan.ToString(); + + // Assert + result.Should().Be(vlanNumber.ToString()); + } + + [Fact] + public void Constructor_WithNullOptionalFields_SetsNullValues() + { + // Act + var vlan = new Vlan(1, 1, "Test", 10, "", null, 0, null); + + // Assert + vlan.EditDate.Should().BeNull(); + vlan.ExtendedData.Should().BeNull(); + } +} diff --git a/classlib.tests/Models/VrfTests.cs b/classlib.tests/Models/VrfTests.cs new file mode 100644 index 0000000..d494dbb --- /dev/null +++ b/classlib.tests/Models/VrfTests.cs @@ -0,0 +1,60 @@ +namespace PS.IPAM.Tests.Models; + +using FluentAssertions; +using PS.IPAM; +using Xunit; + +public class VrfTests +{ + [Fact] + public void Constructor_SetsAllProperties() + { + // Arrange + var id = 1; + var name = "VRF_PROD"; + var rd = "65000:100"; + var description = "Production VRF"; + var sections = "1;2;3"; + var editDate = new DateTime(2026, 1, 10); + var customFields = new Dictionary { { "custom_tenant", "CustomerA" } }; + + // Act + var vrf = new Vrf(id, name, rd, description, sections, editDate, customFields); + + // Assert + vrf.Id.Should().Be(id); + vrf.Name.Should().Be(name); + vrf.RouteDistinguisher.Should().Be(rd); + vrf.Description.Should().Be(description); + vrf.Sections.Should().Be(sections); + vrf.EditDate.Should().Be(editDate); + vrf.ExtendedData.Should().BeEquivalentTo(customFields); + } + + [Theory] + [InlineData("VRF_DEFAULT")] + [InlineData("Production_VRF")] + [InlineData("CUSTOMER_A")] + public void ToString_ReturnsVrfName(string vrfName) + { + // Arrange + var vrf = new Vrf(1, vrfName, "65000:1", "", "", null, null); + + // Act + var result = vrf.ToString(); + + // Assert + result.Should().Be(vrfName); + } + + [Fact] + public void Constructor_WithNullOptionalFields_SetsNullValues() + { + // Act + var vrf = new Vrf(1, "Test", "65000:1", "", "", null, null); + + // Assert + vrf.EditDate.Should().BeNull(); + vrf.ExtendedData.Should().BeNull(); + } +} diff --git a/classlib.tests/classlib.tests.csproj b/classlib.tests/classlib.tests.csproj new file mode 100644 index 0000000..0c4f99c --- /dev/null +++ b/classlib.tests/classlib.tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + latest + false + true + PS.IPAM.Tests + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + From 0b237c6d1cd22bd0a9a6335d373ad067205f5a30 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 14:54:55 +0300 Subject: [PATCH 5/6] Enhance unit tests for Address, Domain, Nameserver, Section, Session, Subnetwork, Tag, Vlan, and Vrf models; implement mock classes for HTTP requests and cmdlet testing. --- .gitea/workflows/ci.yaml | 121 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 .gitea/workflows/ci.yaml diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..324120e --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,121 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + - master + - develop + tags: + - 'v*' + pull_request: + branches: + - main + - master + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build class library + run: dotnet build classlib/classlib.csproj --configuration Release --no-restore + + - name: Build test project + run: dotnet build classlib.tests/classlib.tests.csproj --configuration Release --no-restore + + - name: Run tests + run: dotnet test classlib.tests/classlib.tests.csproj --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage" + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: classlib.tests/TestResults/ + retention-days: 30 + + package: + name: Package Module + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Build Release + run: dotnet build classlib/classlib.csproj --configuration Release + + - name: Create module package + run: | + mkdir -p output/ps.ipam + + # Copy compiled DLL + cp classlib/bin/Release/netstandard2.1/ps.ipam.dll output/ps.ipam/ + + # Copy module manifest and related files + cp ps.ipam.psd1 output/ps.ipam/ + cp ps.ipam.psm1 output/ps.ipam/ + cp LICENSE output/ps.ipam/ + cp README.md output/ps.ipam/ + + # Copy types directory + cp -r types output/ps.ipam/ + + # Copy functions directory + cp -r functions output/ps.ipam/ + + # Copy images directory + cp -r images output/ps.ipam/ + + - name: Upload module artifact + uses: actions/upload-artifact@v4 + with: + name: ps.ipam-module + path: output/ps.ipam/ + retention-days: 90 + + release: + name: Create Release + runs-on: ubuntu-latest + needs: package + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: Download module artifact + uses: actions/download-artifact@v4 + with: + name: ps.ipam-module + path: ps.ipam + + - name: Create release archive + run: | + zip -r ps.ipam-${{ github.ref_name }}.zip ps.ipam/ + tar -czvf ps.ipam-${{ github.ref_name }}.tar.gz ps.ipam/ + + - name: Create Gitea Release + uses: actions/gitea-release-action@v1 + with: + token: ${{ secrets.GITEA_TOKEN }} + files: | + ps.ipam-${{ github.ref_name }}.zip + ps.ipam-${{ github.ref_name }}.tar.gz + title: Release ${{ github.ref_name }} + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} From 6541cf18c00f6a4d4c317211c4590c0af607f1b7 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 19 Jan 2026 16:38:05 +0300 Subject: [PATCH 6/6] Update Gitea CI workflow to use updated token secret for release creation --- .gitea/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 324120e..1e86b2a 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -112,7 +112,7 @@ jobs: - name: Create Gitea Release uses: actions/gitea-release-action@v1 with: - token: ${{ secrets.GITEA_TOKEN }} + token: ${{ secrets.TOKEN }} files: | ps.ipam-${{ github.ref_name }}.zip ps.ipam-${{ github.ref_name }}.tar.gz