diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..1e86b2a --- /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.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, '-') }} 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/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); + } +} 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/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/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.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 + + + + + + + + + + + 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/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 + ); + } + } +} 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.