Skip to content

Commit

Permalink
Merge pull request #91 from d-strobel/feat/dhcp-server-functions
Browse files Browse the repository at this point in the history
Feat/dhcp server functions
  • Loading branch information
d-strobel authored Dec 17, 2024
2 parents 100e2bc + 2b70bdd commit 497c012
Show file tree
Hide file tree
Showing 15 changed files with 1,238 additions and 90 deletions.
3 changes: 3 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gowindows

import (
"github.com/d-strobel/gowindows/connection"
"github.com/d-strobel/gowindows/windows/dhcp"
"github.com/d-strobel/gowindows/windows/dns"
"github.com/d-strobel/gowindows/windows/local/accounts"
)
Expand All @@ -16,6 +17,7 @@ type Client struct {
Connection connection.Connection
LocalAccounts *accounts.Client
Dns *dns.Client
Dhcp *dhcp.Client
}

// NewClient returns a new instance of the Client object, initialized with the provided configuration.
Expand All @@ -29,6 +31,7 @@ func NewClient(conn connection.Connection) *Client {
// Build the client with the subpackages.
c.LocalAccounts = accounts.NewClient(c.Connection)
c.Dns = dns.NewClient(c.Connection)
c.Dhcp = dhcp.NewClient(c.Connection)

return c
}
Expand Down
18 changes: 18 additions & 0 deletions parsing/timespan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package parsing

import (
"fmt"
"time"
)

// PwshTimespanString returns a string representation of a time.Duration that can be used in a PowerShell command.
// The returned string is in the format "$(New-TimeSpan -Days <days> -Hours <hours> -Minutes <minutes> -Seconds <seconds>)".
func PwshTimespanString(d time.Duration) string {
return fmt.Sprintf(
"$(New-TimeSpan -Days %d -Hours %d -Minutes %d -Seconds %d)",
int32(d.Hours())/24,
int32(d.Hours())%24,
int32(d.Minutes())%60,
int32(d.Seconds())%60,
)
}
43 changes: 43 additions & 0 deletions parsing/timespan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package parsing

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestPwshTimespanString(t *testing.T) {
t.Parallel()

tcs := []struct {
description string
inputDuration string
expectedString string
}{
{
"duration of 0",
"0h",
"$(New-TimeSpan -Days 0 -Hours 0 -Minutes 0 -Seconds 0)",
},
{
"duration with 2 days 30 minutes and 15 seconds",
"48h30m15s",
"$(New-TimeSpan -Days 2 -Hours 0 -Minutes 30 -Seconds 15)",
},
{
"duration with 1 day 2 hours 30 minutes and 15 seconds",
"26h30m15s",
"$(New-TimeSpan -Days 1 -Hours 2 -Minutes 30 -Seconds 15)",
},
}

for _, tc := range tcs {
t.Run(tc.description, func(t *testing.T) {
d, err := time.ParseDuration(tc.inputDuration)
assert.NoError(t, err)
actualString := PwshTimespanString(d)
assert.Equal(t, tc.expectedString, actualString)
})
}
}
100 changes: 100 additions & 0 deletions windows/dhcp/dhcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Package dhcp provides a Go library for handling Windows DHCP Server resources.
// The functions are related to the Powershell DhcpServer cmdlets provided by Windows.
// https://learn.microsoft.com/en-us/powershell/module/dhcpserver/?view=windowsserver2022-ps
package dhcp

import (
"context"
"encoding/json"
"net/netip"

"errors"

"github.com/d-strobel/gowindows/connection"
"github.com/d-strobel/gowindows/parsing"
)

// dhcp is a type constraint for the run function, ensuring it works with specific types.
type dhcp interface {
scopeObject
}

// scopeObject is used to unmarshal the JSON output of a scope object.
type scopeObject struct {
Name string `json:"Name"`
Description string `json:"Description"`
ScopeId scopeId `json:"ScopeId"`
StartRange startRange `json:"StartRange"`
EndRange endRange `json:"EndRange"`
SubnetMask subnetMask `json:"SubnetMask"`
State string `json:"State"`
MaxBootpClients uint32 `json:"MaxBootpClients"`
ActivatePolicies bool `json:"ActivatePolicies"`
NapEnable bool `json:"NapEnable"`
NapProfile string `json:"NapProfile"`
Delay uint16 `json:"Delay"`
LeaseDuration parsing.CimTimeDuration `json:"LeaseDuration"`
}
type scopeId struct {
Address netip.Addr `json:"IPAddressToString"`
}
type startRange struct {
Address netip.Addr `json:"IPAddressToString"`
}
type endRange struct {
Address netip.Addr `json:"IPAddressToString"`
}
type subnetMask struct {
Address netip.Addr `json:"IPAddressToString"`
}

// Client represents a client for handling DHCP server functions.
type Client struct {
// Connection represents a connection.Connection object.
Connection connection.Connection

// decodeCliXmlErr represents a function that decodes a CLIXML error and returns aa human readable string.
decodeCliXmlErr func(string) (string, error)
}

// NewClient returns a new instance of the Client.
func NewClient(conn connection.Connection) *Client {
return NewClientWithParser(conn, parsing.DecodeCliXmlErr)
}

// NewClientWithParser returns a new instance of the Client.
// It requires a connection and parsing as input parameters.
func NewClientWithParser(conn connection.Connection, parsing func(string) (string, error)) *Client {
return &Client{Connection: conn, decodeCliXmlErr: parsing}
}

// run runs a PowerShell command against a Windows system, handles the command results,
// and unmarshals the output into a local object type.
func run[T dhcp](ctx context.Context, c *Client, cmd string, t *T) error {
// Run the command
result, err := c.Connection.RunWithPowershell(ctx, cmd)
if err != nil {
return err
}

// Handle stderr
if result.StdErr != "" {
stderr, err := c.decodeCliXmlErr(result.StdErr)
if err != nil {
return err
}

return errors.New(stderr)
}

if result.StdOut == "" {
return nil
}

// Unmarshal stdout
if err = json.Unmarshal([]byte(result.StdOut), &t); err != nil {
return err
}

return nil
}
53 changes: 53 additions & 0 deletions windows/dhcp/dhcp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package dhcp

import (
"context"
"testing"

"github.com/d-strobel/gowindows/connection"
mockConnection "github.com/d-strobel/gowindows/connection/mocks"
"github.com/stretchr/testify/suite"
)

// Unit test suite for all dhcp functions
type DhcpServerUnitTestSuite struct {
suite.Suite
}

// Run all dhcp unit tests
func TestDhcpServerUnitTestSuite(t *testing.T) {
suite.Run(t, &DhcpServerUnitTestSuite{})
}

func (suite *DhcpServerUnitTestSuite) TestNewClient() {
suite.Run("should return a new dhcp client", func() {
mockConn := mockConnection.NewMockConnection(suite.T())
mockDecodeCliXmlErr := func(s string) (string, error) { return "", nil }
actualClient := NewClientWithParser(mockConn, mockDecodeCliXmlErr)
expectedClient := &Client{Connection: mockConn, decodeCliXmlErr: mockDecodeCliXmlErr}
suite.IsType(expectedClient, actualClient)
suite.Equal(expectedClient.Connection, actualClient.Connection)
})
}

func (suite *DhcpServerUnitTestSuite) TestDhcpRun() {
suite.T().Parallel()

suite.Run("should return an unmarshalled scope object", func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mockConn := mockConnection.NewMockConnection(suite.T())
c := &Client{
Connection: mockConn,
decodeCliXmlErr: func(s string) (string, error) { return "", nil },
}
cmd := "Get-DhcpServerv4Scope -ScopeId '192.168.10.0' | ConvertTo-Json -Compress"
mockConn.EXPECT().
RunWithPowershell(ctx, cmd).
Return(connection.CmdResult{StdOut: scopeV4Json}, nil)
var o scopeObject
err := run(ctx, c, cmd, &o)
suite.NoError(err)
suite.Equal(expectedScopeObject, o)
})
}
Loading

0 comments on commit 497c012

Please sign in to comment.