6 Commits

20 changed files with 176 additions and 650 deletions
-126
View File
@@ -1,126 +0,0 @@
name: Build and Release
on:
push:
branches: [ main ]
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/**'
- '!.github/workflows/build-and-release.yml'
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
release:
description: 'Create a release'
required: false
default: false
type: boolean
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
- name: Setup PowerShell
uses: actions/setup-powershell@v1
with:
powershell-version: '7.2'
- name: Restore dependencies
run: dotnet restore src/PSOPNSenseAPI.sln
- name: Run tests
run: pwsh -Command "./build/test.ps1 -Configuration Release"
- name: Build and package
run: |
pwsh -Command "./build/build-release.ps1 -Clean -Package"
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: PSOPNSenseAPI
path: output/PSOPNSenseAPI/
- name: Upload package artifacts
uses: actions/upload-artifact@v3
with:
name: PSOPNSenseAPI-Package
path: packages/*.zip
release:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event.inputs.release == 'true'
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup PowerShell
uses: actions/setup-powershell@v1
with:
powershell-version: '7.2'
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: PSOPNSenseAPI
path: output/PSOPNSenseAPI/
- name: Download package artifacts
uses: actions/download-artifact@v3
with:
name: PSOPNSenseAPI-Package
path: packages/
- name: Configure Git
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
- name: Create Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$version = Get-Date -Format "yyyy.MM.dd.HHmm"
# Update version in project file
$projectFile = "src/PSOPNSenseAPI/PSOPNSenseAPI.csproj"
$projectXml = [xml](Get-Content $projectFile)
$versionNode = $projectXml.Project.PropertyGroup.Version
$versionNode.InnerText = $version
$projectXml.Save($projectFile)
# Update version in module manifest
$manifestFile = "PSOPNSenseAPI/PSOPNSenseAPI.psd1"
$manifestContent = Get-Content -Path $manifestFile -Raw
$manifestContent = $manifestContent -replace "ModuleVersion\s*=\s*['`"].*['`"]", "ModuleVersion = '$version'"
Set-Content -Path $manifestFile -Value $manifestContent
# Commit changes
git add $projectFile
git add $manifestFile
git commit -m "Release version $version [skip ci]"
git push
# Create release
$packagePath = Get-ChildItem -Path "packages/*.zip" | Select-Object -First 1 -ExpandProperty FullName
gh release create "v$version" `
--title "Release v$version" `
--notes "Release version $version - $(Get-Date -Format 'yyyy-MM-dd')" `
$packagePath
shell: pwsh
+2
View File
@@ -1,3 +1,5 @@
.vs/
bin/
obj/
*.zip
output/*.zip
+87 -8
View File
@@ -6,6 +6,8 @@ This component provides cmdlets for managing Tailscale VPN on OPNSense firewalls
The Tailscale Integration component allows you to install, configure, and manage Tailscale VPN on OPNSense firewalls. It provides cmdlets for enabling Tailscale, configuring subnet routes, and managing the Tailscale connection.
The module will automatically install the Tailscale plugin if it doesn't exist, making it easy to set up Tailscale on a new OPNSense firewall.
## Cmdlets
### Get-OPNSenseTailscaleStatus
@@ -138,6 +140,53 @@ Apply-OPNSenseTailscaleChanges
Apply-OPNSenseTailscaleChanges -Force
```
### Connect-OPNSenseTailscale
Connects to the Tailscale network using an authentication key.
#### Parameters
- **AuthKey** - The Tailscale authentication key.
- **InstallIfMissing** - Installs the Tailscale plugin if it's not already installed.
- **EnableIfDisabled** - Enables Tailscale if it's disabled.
- **StartIfStopped** - Starts the Tailscale service if it's stopped.
- **SubnetRoutes** - The subnet routes to advertise to Tailscale.
- **AdvertiseRoutes** - Whether to advertise routes to Tailscale.
- **Force** - Suppresses the confirmation prompt.
- **PassThru** - Returns the Tailscale status.
#### Examples
```powershell
# Connect to Tailscale with an auth key
Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456"
# Connect to Tailscale with automatic installation and enablement
Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -InstallIfMissing -EnableIfDisabled -StartIfStopped
# Connect to Tailscale and advertise subnet routes
Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -SubnetRoutes "192.168.1.0/24","10.0.0.0/8" -AdvertiseRoutes
```
### Disconnect-OPNSenseTailscale
Disconnects from the Tailscale network.
#### Parameters
- **Force** - Suppresses the confirmation prompt.
- **PassThru** - Returns the Tailscale status.
#### Examples
```powershell
# Disconnect from Tailscale
Disconnect-OPNSenseTailscale
# Disconnect from Tailscale without confirmation
Disconnect-OPNSenseTailscale -Force
```
## Common Scenarios
### Installing and Configuring Tailscale
@@ -159,6 +208,19 @@ Apply-OPNSenseTailscaleChanges -Force
Disconnect-OPNSense
```
### Simplified Installation and Connection
```powershell
# Connect to the OPNSense firewall
Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck
# Connect to Tailscale with automatic installation and enablement
Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -InstallIfMissing -EnableIfDisabled -StartIfStopped -Force
# Disconnect from the firewall
Disconnect-OPNSense
```
### Configuring Tailscale as a Subnet Router
```powershell
@@ -177,14 +239,8 @@ foreach ($interface in $interfaces) {
}
}
# Join the subnets with commas
$advertisedRoutes = $subnets -join ","
# Enable Tailscale with route advertisement
Enable-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -AdvertiseRoutes $advertisedRoutes -Force
# Apply the changes
Apply-OPNSenseTailscaleChanges -Force
# Connect to Tailscale with subnet route advertisement
Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -SubnetRoutes $subnets -AdvertiseRoutes -InstallIfMissing -Force
# Disconnect from the firewall
Disconnect-OPNSense
@@ -206,6 +262,26 @@ Apply-OPNSenseTailscaleChanges -Force
Disconnect-OPNSense
```
### Connecting and Disconnecting from Tailscale
```powershell
# Connect to the OPNSense firewall
Connect-OPNSense -Server "https://firewall.example.com" -ApiKey "your_api_key" -ApiSecret "your_api_secret" -SkipCertificateCheck
# Connect to Tailscale
Connect-OPNSenseTailscale -AuthKey "tskey-auth-abcdef123456" -InstallIfMissing -EnableIfDisabled -StartIfStopped -Force
# Get Tailscale status
$status = Get-OPNSenseTailscaleStatus
Write-Output "Tailscale Status: $($status.Status)"
# Disconnect from Tailscale
Disconnect-OPNSenseTailscale -Force
# Disconnect from the firewall
Disconnect-OPNSense
```
### Updating Tailscale Routes
```powershell
@@ -253,6 +329,7 @@ Disconnect-OPNSense
- Tailscale requires the Tailscale plugin to be installed on the OPNSense firewall.
- The `Install-OPNSenseTailscale` cmdlet will install the plugin if it's not already installed.
- The `Connect-OPNSenseTailscale` cmdlet can automatically install the plugin, enable Tailscale, and start the service with the `-InstallIfMissing`, `-EnableIfDisabled`, and `-StartIfStopped` parameters.
- Tailscale authentication keys can be generated from the Tailscale admin console.
- Authentication keys are single-use and expire after a short period.
- When advertising routes, ensure that the routes are valid and do not overlap with Tailscale's internal routes.
@@ -261,3 +338,5 @@ Disconnect-OPNSense
- The `ExitNode` option allows other Tailscale nodes to use the OPNSense firewall as an internet gateway.
- Changes to Tailscale settings are not applied until you call `Apply-OPNSenseTailscaleChanges`.
- Consider using the `-PassThru` parameter when updating Tailscale settings to verify the changes.
- The `Connect-OPNSenseTailscale` cmdlet combines multiple steps (installation, enablement, and connection) into a single command.
- The `Disconnect-OPNSenseTailscale` cmdlet disconnects from the Tailscale network but does not disable Tailscale.
+35 -10
View File
@@ -11,6 +11,7 @@ The PSOPNSenseAPI solution is a Visual Studio solution that contains the source
The solution contains the following projects:
- **PSOPNSenseAPI**: The main project containing the PowerShell module code.
- **PSOPNSenseAPI.Tests**: The test project containing unit tests for the PowerShell module.
## Opening the Solution
@@ -56,19 +57,19 @@ If you encounter issues with the IPNetwork2 library, here are some common troubl
1. **Check the reference**: Ensure that the IPNetwork2 package is properly referenced in the project file.
```xml
<PackageReference Include="IPNetwork2" Version="2.6.618" />
<PackageReference Include="IPNetwork2" Version="3.1.764" />
```
2. **Verify the namespace**: The correct namespace for IPNetwork2 is `System.Net.IPNetwork`.
2. **Verify the namespace**: The correct namespace for IPNetwork2 is `System.Net`.
```csharp
using System.Net.IPNetwork;
using System.Net;
```
3. **Check the class name**: The main class in IPNetwork2 is `IPNetwork`, not `IPNetwork2`.
```csharp
// Correct usage
System.Net.IPNetwork.IPNetwork network = System.Net.IPNetwork.IPNetwork.Parse("192.168.1.0/24");
System.Net.IPNetwork network = System.Net.IPNetwork.Parse("192.168.1.0/24");
// Incorrect usage
IPNetwork2 network = IPNetwork2.Parse("192.168.1.0/24");
```
@@ -100,7 +101,7 @@ When adding new features to the solution:
2. Implement service classes in the `Services` directory
3. Create PowerShell cmdlets in the `Cmdlets` directory
4. Update the module manifest (`PSOPNSenseAPI.psd1`) to export the new cmdlets
5. Add tests for the new features
5. Add tests for the new features in the `PSOPNSenseAPI.Tests` project
6. Update documentation
## Testing
@@ -112,9 +113,14 @@ The solution includes unit tests that can be run from Visual Studio using the Te
.\build\test.ps1
```
## Continuous Integration
## Manual Release Process
The project uses GitHub Actions for continuous integration. The CI workflow builds and tests the solution on every push to the main branch.
The project uses a manual release process. To create a new release:
1. Update the version in the project file and module manifest
2. Build the solution in Release mode
3. Create a zip file of the module
4. Create a GitHub release with the zip file attached
## Dependencies
@@ -123,10 +129,29 @@ The solution has the following dependencies:
- PowerShellStandard.Library (5.1.1)
- Newtonsoft.Json (13.0.3)
- System.Net.Http (4.3.4)
- IPNetwork2 (2.6.618)
- IPNetwork2 (3.1.764)
## Notes
- The solution is configured to target both .NET Framework 4.7.2 and .NET Standard 2.0 to support both Windows PowerShell 5.1 and PowerShell 7+.
- The module uses a versioning scheme of `yyyy.MM.dd.HHmm` for releases, which is automatically generated during the build process.
- The module uses a versioning scheme of `yyyy.MM.dd.HHmm` for releases.
- The solution includes XML documentation comments that are used to generate help content for the PowerShell cmdlets.
- The module uses System.Reflection.Assembly::LoadBytes to load DLLs to avoid file lock issues.
- DLLs are organized in a lib subfolder for neatness.
## Module Structure
The module has the following structure:
```
PSOPNSenseAPI/
├── lib/ # DLL files
│ ├── Newtonsoft.Json.dll
│ ├── PSOPNSenseAPI.dll
│ ├── System.Net.IPNetwork.dll
│ └── ...
├── PSOPNSenseAPI.psd1 # Module manifest
└── PSOPNSenseAPI.psm1 # Module script that loads DLLs
```
The PSM1 file uses System.Reflection.Assembly::LoadBytes to load the DLLs from the lib folder, which avoids file lock issues that can occur when PowerShell loads DLLs directly.
+2 -2
View File
@@ -1,6 +1,6 @@
@{
RootModule = 'PSOPNSenseAPI.dll'
ModuleVersion = '2025.04.14.2246'
RootModule = 'PSOPNSenseAPI.psm1'
ModuleVersion = '2025.04.14.2320'
GUID = '1f0e4b77-cc7c-4a1e-b45a-d7c51a3c562e'
Author = 'PSOPNSenseAPI Contributors'
CompanyName = 'PSOPNSenseAPI'
+25
View File
@@ -0,0 +1,25 @@
# Load assemblies from the lib folder using System.Reflection.Assembly::LoadBytes
# This avoids file lock issues when the module is loaded
$libPath = Join-Path $PSScriptRoot "lib"
$dllFiles = Get-ChildItem -Path $libPath -Filter "*.dll"
foreach ($dll in $dllFiles) {
try {
$dllBytes = [System.IO.File]::ReadAllBytes($dll.FullName)
$Null = [System.Reflection.Assembly]::Load($dllBytes)
Write-Verbose "Attempting to load assembly: $($dll.FullName)"
}
catch {
Write-Warning "Failed to load assembly $($dll.Name): $_"
}
}
# Export all cmdlets
$cmdlets = [System.AppDomain]::CurrentDomain.GetAssemblies() |
Where-Object { $_.FullName -like "*PSOPNSenseAPI*" } |
ForEach-Object { $_.GetTypes() } |
Where-Object { $_.IsPublic -and $_.IsClass -and $_.Name -like "*Cmdlet" } |
ForEach-Object { $_.Name }
Export-ModuleMember -Cmdlet $cmdlets
+12
View File
@@ -0,0 +1,12 @@
## What's New
### Added
- Port forwarding management (Get, New, Set, Remove)
- Added unit tests for interface management
### Changed
- Updated IPNetwork2 library from version 2.6.618 to 3.1.764
- Updated code to use the new namespace (System.Net) for IPNetwork2
- Added support for .NET 8.0 and .NET 9.0
- Improved XML documentation for all classes and methods
- Made OPNSenseApiClient more testable by introducing an interface
+1 -1
View File
@@ -4,7 +4,7 @@
<TargetFrameworks>netstandard2.0;net472</TargetFrameworks>
<AssemblyName>PSOPNSenseAPI</AssemblyName>
<RootNamespace>PSOPNSenseAPI</RootNamespace>
<Version>2025.04.14.2246</Version>
<Version>2025.04.14.2320</Version>
<Authors>PSOPNSenseAPI Contributors</Authors>
<Company>PSOPNSenseAPI</Company>
<Description>PowerShell module for interacting with the OPNSense API to configure firewalls</Description>
+3 -3
View File
@@ -1,9 +1,9 @@
@{
# Script module or binary module file associated with this manifest.
RootModule = 'PSOPNSenseAPI.dll'
RootModule = 'PSOPNSenseAPI.psm1'
# Version number of this module.
ModuleVersion = '0.5.0'
ModuleVersion = '2025.04.14.2320'
# Supported PSEditions
CompatiblePSEditions = @('Desktop', 'Core')
@@ -45,7 +45,7 @@
# RequiredModules = @()
# Assemblies that must be loaded prior to importing this module
RequiredAssemblies = @('System.Net.Http', 'Newtonsoft.Json', 'System.Net.IPNetwork')
# RequiredAssemblies = @()
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
@@ -1,202 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PSOPNSenseAPI.Models;
namespace PSOPNSenseAPI.Services
{
/// <summary>
/// Service for managing port forwarding rules in OPNSense
/// </summary>
public class PortForwardingService : ServiceBase
{
/// <summary>
/// Initializes a new instance of the <see cref="PortForwardingService"/> class
/// </summary>
/// <param name="apiClient">The API client to use for requests</param>
public PortForwardingService(ApiClient apiClient) : base(apiClient)
{
}
/// <summary>
/// Gets all port forwarding rules
/// </summary>
/// <returns>A list of port forwarding rules</returns>
public async Task<List<PortForwardingRule>> GetPortForwardingRulesAsync()
{
var response = await ApiClient.GetAsync("/firewall/nat/getRule");
var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JObject.Parse(responseContent);
if (jsonResponse["status"]?.ToString() != "ok")
{
throw new Exception($"Failed to get port forwarding rules: {jsonResponse["status_msg"]}");
}
var rules = new List<PortForwardingRule>();
var rulesData = jsonResponse["rows"];
foreach (var rule in rulesData)
{
var portForwardingRule = new PortForwardingRule
{
Uuid = rule["uuid"]?.ToString(),
Enabled = rule["enabled"]?.ToString() == "1",
Description = rule["descr"]?.ToString(),
Interface = rule["interface"]?.ToString(),
Protocol = rule["protocol"]?.ToString(),
Source = rule["source"]?.ToString(),
SourcePort = rule["source_port"]?.ToString(),
Destination = rule["destination"]?.ToString(),
DestinationPort = rule["destination_port"]?.ToString(),
TargetIP = rule["target"]?.ToString(),
TargetPort = rule["target_port"]?.ToString(),
NatReflection = rule["natreflection"]?.ToString(),
FilterRuleAssociation = rule["associated-rule-id"]?.ToString() != "none",
Log = rule["log"]?.ToString() == "1"
};
rules.Add(portForwardingRule);
}
return rules;
}
/// <summary>
/// Gets a port forwarding rule by UUID
/// </summary>
/// <param name="uuid">The UUID of the rule to get</param>
/// <returns>The port forwarding rule</returns>
public async Task<PortForwardingRule> GetPortForwardingRuleAsync(string uuid)
{
var rules = await GetPortForwardingRulesAsync();
return rules.Find(r => r.Uuid == uuid);
}
/// <summary>
/// Creates a new port forwarding rule
/// </summary>
/// <param name="rule">The rule to create</param>
/// <returns>The UUID of the created rule</returns>
public async Task<string> CreatePortForwardingRuleAsync(PortForwardingRule rule)
{
var ruleData = new
{
rule = new
{
enabled = rule.Enabled ? "1" : "0",
descr = rule.Description,
interface = rule.Interface,
protocol = rule.Protocol,
source = rule.Source,
source_port = rule.SourcePort,
destination = rule.Destination,
destination_port = rule.DestinationPort,
target = rule.TargetIP,
target_port = rule.TargetPort,
natreflection = rule.NatReflection,
associated_rule_id = rule.FilterRuleAssociation ? "pass" : "none",
log = rule.Log ? "1" : "0"
}
};
var response = await ApiClient.PostAsync("/firewall/nat/addRule", ruleData);
var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JObject.Parse(responseContent);
if (jsonResponse["status"]?.ToString() != "ok")
{
throw new Exception($"Failed to create port forwarding rule: {jsonResponse["status_msg"]}");
}
// Apply changes
await ApplyChangesAsync();
return jsonResponse["uuid"]?.ToString();
}
/// <summary>
/// Updates an existing port forwarding rule
/// </summary>
/// <param name="rule">The rule to update</param>
/// <returns>True if successful</returns>
public async Task<bool> UpdatePortForwardingRuleAsync(PortForwardingRule rule)
{
if (string.IsNullOrEmpty(rule.Uuid))
{
throw new ArgumentException("Rule UUID cannot be null or empty", nameof(rule));
}
var ruleData = new
{
rule = new
{
enabled = rule.Enabled ? "1" : "0",
descr = rule.Description,
interface = rule.Interface,
protocol = rule.Protocol,
source = rule.Source,
source_port = rule.SourcePort,
destination = rule.Destination,
destination_port = rule.DestinationPort,
target = rule.TargetIP,
target_port = rule.TargetPort,
natreflection = rule.NatReflection,
associated_rule_id = rule.FilterRuleAssociation ? "pass" : "none",
log = rule.Log ? "1" : "0"
}
};
var response = await ApiClient.PostAsync($"/firewall/nat/setRule/{rule.Uuid}", ruleData);
var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JObject.Parse(responseContent);
if (jsonResponse["status"]?.ToString() != "ok")
{
throw new Exception($"Failed to update port forwarding rule: {jsonResponse["status_msg"]}");
}
// Apply changes
await ApplyChangesAsync();
return true;
}
/// <summary>
/// Deletes a port forwarding rule
/// </summary>
/// <param name="uuid">The UUID of the rule to delete</param>
/// <returns>True if successful</returns>
public async Task<bool> DeletePortForwardingRuleAsync(string uuid)
{
var response = await ApiClient.PostAsync($"/firewall/nat/delRule/{uuid}", null);
var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JObject.Parse(responseContent);
if (jsonResponse["status"]?.ToString() != "ok")
{
throw new Exception($"Failed to delete port forwarding rule: {jsonResponse["status_msg"]}");
}
// Apply changes
await ApplyChangesAsync();
return true;
}
/// <summary>
/// Applies the changes to the firewall
/// </summary>
/// <returns>True if successful</returns>
private async Task<bool> ApplyChangesAsync()
{
var response = await ApiClient.PostAsync("/firewall/nat/apply", null);
var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JObject.Parse(responseContent);
return jsonResponse["status"]?.ToString() == "ok";
}
}
}
-289
View File
@@ -1,289 +0,0 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using PSOPNSenseAPI.Logging;
using PSOPNSenseAPI.Models;
namespace PSOPNSenseAPI.Services
{
public class InterfaceService
{
private readonly OPNSenseApiClient _apiClient;
private readonly ILogger _logger;
public InterfaceService(OPNSenseApiClient apiClient, ILogger logger)
{
_apiClient = apiClient;
_logger = logger;
}
public async Task<InterfaceListResponse> GetInterfacesAsync()
{
_logger.Information("Getting interfaces");
var endpoint = "interfaces/overview/searchInterfaces";
return await _apiClient.GetAsync<InterfaceListResponse>(endpoint);
}
public async Task<InterfaceDetailResponse> GetInterfaceDetailAsync(string interfaceName)
{
_logger.Information($"Getting interface {interfaceName}");
var endpoint = $"interfaces/overview/getInterface/{interfaceName}";
return await _apiClient.GetAsync<InterfaceDetailResponse>(endpoint);
}
public async Task<InterfaceStatisticsResponse> GetInterfaceStatisticsAsync()
{
_logger.Information("Getting interface statistics");
var endpoint = "diagnostics/interface/getInterfaceStatistics";
return await _apiClient.GetAsync<InterfaceStatisticsResponse>(endpoint);
}
public async Task<InterfaceUpdateResponse> UpdateInterfaceAsync(string interfaceName, InterfaceConfig interfaceConfig)
{
_logger.Information($"Updating interface {interfaceName}");
var endpoint = $"interfaces/overview/setInterface/{interfaceName}";
var data = new { interface = interfaceConfig };
return await _apiClient.PostAsync<InterfaceUpdateResponse>(endpoint, data);
}
public async Task<InterfaceRestartResponse> RestartInterfaceAsync(string interfaceName)
{
_logger.Information($"Restarting interface {interfaceName}");
var endpoint = $"interfaces/overview/reconfigure/{interfaceName}";
return await _apiClient.PostAsync<InterfaceRestartResponse>(endpoint);
}
public async Task<VLANListResponse> GetVLANsAsync()
{
_logger.Information("Getting VLANs");
var endpoint = "interfaces/vlan/searchItem";
return await _apiClient.GetAsync<VLANListResponse>(endpoint);
}
public async Task<VLANResponse> GetVLANAsync(string uuid)
{
_logger.Information($"Getting VLAN with UUID {uuid}");
var endpoint = $"interfaces/vlan/getItem/{uuid}";
return await _apiClient.GetAsync<VLANResponse>(endpoint);
}
public async Task<VLANCreateResponse> CreateVLANAsync(VLANConfig vlan)
{
_logger.Information("Creating VLAN");
var endpoint = "interfaces/vlan/addItem";
var data = new { vlan = vlan };
return await _apiClient.PostAsync<VLANCreateResponse>(endpoint, data);
}
public async Task<VLANUpdateResponse> UpdateVLANAsync(string uuid, VLANConfig vlan)
{
_logger.Information($"Updating VLAN with UUID {uuid}");
var endpoint = $"interfaces/vlan/setItem/{uuid}";
var data = new { vlan = vlan };
return await _apiClient.PostAsync<VLANUpdateResponse>(endpoint, data);
}
public async Task<VLANDeleteResponse> DeleteVLANAsync(string uuid)
{
_logger.Information($"Deleting VLAN with UUID {uuid}");
var endpoint = $"interfaces/vlan/delItem/{uuid}";
return await _apiClient.PostAsync<VLANDeleteResponse>(endpoint);
}
}
public class InterfaceListResponse
{
[JsonProperty("rows")]
public List<InterfaceInfo> Interfaces { get; set; }
}
public class InterfaceInfo
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("ipaddr")]
public string IpAddress { get; set; }
[JsonProperty("subnet")]
public string SubnetMask { get; set; }
[JsonProperty("gateway")]
public string Gateway { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
}
public class InterfaceDetailResponse
{
[JsonProperty("interface")]
public InterfaceDetail Interface { get; set; }
}
public class InterfaceDetail
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("ipaddr")]
public string IpAddress { get; set; }
[JsonProperty("subnet")]
public string SubnetMask { get; set; }
[JsonProperty("gateway")]
public string Gateway { get; set; }
[JsonProperty("enable")]
public string Enabled { get; set; }
}
public class InterfaceConfig
{
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("ipaddr")]
public string IpAddress { get; set; }
[JsonProperty("subnet")]
public string SubnetMask { get; set; }
[JsonProperty("gateway")]
public string Gateway { get; set; }
[JsonProperty("enable")]
public string Enabled { get; set; }
}
public class InterfaceUpdateResponse
{
[JsonProperty("result")]
public string Result { get; set; }
}
public class InterfaceRestartResponse
{
[JsonProperty("status")]
public string Status { get; set; }
}
public class InterfaceStatisticsResponse
{
[JsonProperty("statistics")]
public Dictionary<string, InterfaceStatistics> Statistics { get; set; }
}
public class InterfaceStatistics
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("inpkts")]
public string InPackets { get; set; }
[JsonProperty("outpkts")]
public string OutPackets { get; set; }
[JsonProperty("inbytes")]
public string InBytes { get; set; }
[JsonProperty("outbytes")]
public string OutBytes { get; set; }
}
public class VLANListResponse
{
[JsonProperty("rows")]
public List<VLAN> Rows { get; set; }
}
public class VLAN
{
[JsonProperty("uuid")]
public string Uuid { get; set; }
[JsonProperty("if")]
public string Interface { get; set; }
[JsonProperty("tag")]
public string Tag { get; set; }
[JsonProperty("pcp")]
public string Priority { get; set; }
[JsonProperty("descr")]
public string Description { get; set; }
}
public class VLANResponse
{
[JsonProperty("vlan")]
public VLANDetail Vlan { get; set; }
}
public class VLANDetail
{
[JsonProperty("if")]
public string Interface { get; set; }
[JsonProperty("tag")]
public string Tag { get; set; }
[JsonProperty("pcp")]
public string Priority { get; set; }
[JsonProperty("descr")]
public string Description { get; set; }
}
public class VLANConfig
{
[JsonProperty("if")]
public string Interface { get; set; }
[JsonProperty("tag")]
public string Tag { get; set; }
[JsonProperty("pcp")]
public string Priority { get; set; }
[JsonProperty("descr")]
public string Description { get; set; }
}
public class VLANCreateResponse
{
[JsonProperty("uuid")]
public string Uuid { get; set; }
}
public class VLANUpdateResponse
{
[JsonProperty("result")]
public string Result { get; set; }
}
public class VLANDeleteResponse
{
[JsonProperty("result")]
public string Result { get; set; }
}
}
@@ -11,9 +11,9 @@ namespace PSOPNSenseAPI.Tests
[TestClass]
public class InterfaceServiceTests
{
private Mock<IOPNSenseApiClient> _mockApiClient;
private Mock<ILogger> _mockLogger;
private InterfaceService _interfaceService;
private Mock<IOPNSenseApiClient>? _mockApiClient;
private Mock<ILogger>? _mockLogger;
private InterfaceService? _interfaceService;
[TestInitialize]
public void Setup()
@@ -43,11 +43,11 @@ namespace PSOPNSenseAPI.Tests
}
};
_mockApiClient.Setup(x => x.GetAsync<InterfaceListResponse>("interfaces/overview/searchInterfaces"))
_mockApiClient!.Setup(x => x.GetAsync<InterfaceListResponse>("interfaces/overview/searchInterfaces"))
.ReturnsAsync(response);
// Act
var result = await _interfaceService.GetInterfacesAsync();
var result = await _interfaceService!.GetInterfacesAsync();
// Assert
Assert.IsNotNull(result);
@@ -78,11 +78,11 @@ namespace PSOPNSenseAPI.Tests
}
};
_mockApiClient.Setup(x => x.GetAsync<InterfaceDetailResponse>($"interfaces/overview/getInterface/{interfaceName}"))
_mockApiClient!.Setup(x => x.GetAsync<InterfaceDetailResponse>($"interfaces/overview/getInterface/{interfaceName}"))
.ReturnsAsync(response);
// Act
var result = await _interfaceService.GetInterfaceDetailAsync(interfaceName);
var result = await _interfaceService!.GetInterfaceDetailAsync(interfaceName);
// Assert
Assert.IsNotNull(result);
@@ -114,11 +114,11 @@ namespace PSOPNSenseAPI.Tests
Result = "saved"
};
_mockApiClient.Setup(x => x.PostAsync<InterfaceUpdateResponse>($"interfaces/overview/setInterface/{interfaceName}", It.IsAny<object>()))
_mockApiClient!.Setup(x => x.PostAsync<InterfaceUpdateResponse>($"interfaces/overview/setInterface/{interfaceName}", It.IsAny<object>()))
.ReturnsAsync(response);
// Act
var result = await _interfaceService.UpdateInterfaceAsync(interfaceName, interfaceConfig);
var result = await _interfaceService!.UpdateInterfaceAsync(interfaceName, interfaceConfig);
// Assert
Assert.IsNotNull(result);