Skip to content

Commit

Permalink
rework new mechanism with the script
Browse files Browse the repository at this point in the history
  • Loading branch information
f18m committed Oct 23, 2024
1 parent 6941b33 commit 943e460
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 118 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dhcp-clients-webapp-backend/bin/
dhcp-clients-webapp-backend/tmp/
.docker-image

test-db.sqlite3
60 changes: 25 additions & 35 deletions dhcp-clients-webapp-backend/pkg/trackerdb/trackerdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,6 @@ import (
_ "github.com/mattn/go-sqlite3"
)

// DhcpClient represents the structure for a DHCP client.
// The DHCP client might be currently connected to the server or not; in other words this
// may represent a DHCP client that has been connected in the past, but currently is not.
type DhcpClient struct {
MacAddr net.HardwareAddr `json:"mac_addr"`
Hostname string `json:"hostname"`
HasStaticIP bool `json:"has_static_ip"`
FriendlyName string `json:"friendly_name"`
LastSeen time.Time `json:"last_seen"`
}

// DhcpClientTrackerDB manages the database operations for DHCP clients.
type DhcpClientTrackerDB struct {
DB *sql.DB
Expand All @@ -34,20 +23,7 @@ func NewDhcpClientTrackerDB(dbPath string) (*DhcpClientTrackerDB, error) {
return nil, err
}

// Create table if not exists
createTableQuery := `
CREATE TABLE IF NOT EXISTS dhcp_clients (
mac_addr TEXT PRIMARY KEY,
hostname TEXT,
has_static_ip INTEGER,
friendly_name TEXT,
last_seen TEXT
);
`
_, err = db.Exec(createTableQuery)
if err != nil {
return nil, err
}
// the table should be already there as it's created by the dnsmasq helper script

return &DhcpClientTrackerDB{DB: db}, nil
}
Expand All @@ -59,6 +35,21 @@ func NewTestDB() DhcpClientTrackerDB {
if err != nil {
log.Fatal("Failed to initialize test database")
}

// Create table
createTableQuery := `
CREATE TABLE IF NOT EXISTS dhcp_clients (
mac_addr TEXT PRIMARY KEY,
hostname TEXT,
last_seen TEXT,
dhcp_server_start_counter INT
);
`
_, err = db.DB.Exec(createTableQuery)
if err != nil {
log.Fatal("Failed to initialize test database")
}

return *db
}

Expand All @@ -79,16 +70,15 @@ func NewTestDBWithData(clientsInDB []DhcpClient) DhcpClientTrackerDB {
// TrackNewDhcpClient inserts a new DHCP client into the database.
func (d *DhcpClientTrackerDB) TrackNewDhcpClient(client DhcpClient) error {
insertQuery := `
INSERT INTO dhcp_clients (mac_addr, hostname, has_static_ip, friendly_name, last_seen)
VALUES (?, ?, ?, ?, ?)
INSERT INTO dhcp_clients (mac_addr, hostname, last_seen, dhcp_server_start_counter)
VALUES (?, ?, ?, ?)
ON CONFLICT(mac_addr) DO UPDATE SET
hostname=excluded.hostname,
has_static_ip=excluded.has_static_ip,
friendly_name=excluded.friendly_name,
last_seen=excluded.last_seen;
last_seen=excluded.last_seen,
dhcp_server_start_counter=excluded.dhcp_server_start_counter;
`

_, err := d.DB.Exec(insertQuery, client.MacAddr.String(), client.Hostname, client.HasStaticIP, client.FriendlyName, client.LastSeen.Format(time.RFC3339))
_, err := d.DB.Exec(insertQuery, client.MacAddr.String(), client.Hostname, client.LastSeen.Format(time.RFC3339), 0)
if err != nil {
return err
}
Expand All @@ -98,14 +88,14 @@ func (d *DhcpClientTrackerDB) TrackNewDhcpClient(client DhcpClient) error {

// GetDhcpClient retrieves a DHCP client by its MAC address.
func (d *DhcpClientTrackerDB) GetDhcpClient(macAddr net.HardwareAddr) (*DhcpClient, error) {
query := `SELECT mac_addr, hostname, has_static_ip, friendly_name, last_seen FROM dhcp_clients WHERE mac_addr = ?`
query := `SELECT mac_addr, hostname, last_seen FROM dhcp_clients WHERE mac_addr = ?`
row := d.DB.QueryRow(query, macAddr.String())

var client DhcpClient
var lastSeen string
var mac string

err := row.Scan(&mac, &client.Hostname, &client.HasStaticIP, &client.FriendlyName, &lastSeen)
err := row.Scan(&mac, &client.Hostname, &lastSeen)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("client with mac_addr %s not found", macAddr)
Expand Down Expand Up @@ -159,7 +149,7 @@ func (d *DhcpClientTrackerDB) UnmarshalDhcpClient(data string) error {
// which identifies the currently-alive DHCP clients.
func (d *DhcpClientTrackerDB) GetDeadDhcpClients(aliveClients []net.HardwareAddr) ([]DhcpClient, error) {
// Step 1: Get all DHCP clients from the database
rows, err := d.DB.Query("SELECT mac_addr, hostname, has_static_ip, friendly_name, last_seen FROM dhcp_clients")
rows, err := d.DB.Query("SELECT mac_addr, hostname, last_seen FROM dhcp_clients")
if err != nil {
return nil, fmt.Errorf("failed to query dhcp_clients: %v", err)
}
Expand All @@ -179,7 +169,7 @@ func (d *DhcpClientTrackerDB) GetDeadDhcpClients(aliveClients []net.HardwareAddr
var mac string

// Scan the row data into the DhcpClient struct
err := rows.Scan(&mac, &client.Hostname, &client.HasStaticIP, &client.FriendlyName, &lastSeenStr)
err := rows.Scan(&mac, &client.Hostname, &lastSeenStr)
if err != nil {
return nil, fmt.Errorf("failed to scan row: %v", err)
}
Expand Down
58 changes: 29 additions & 29 deletions dhcp-clients-webapp-backend/pkg/trackerdb/trackerdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ func CompareDhcpClientSlices(slice1, slice2 []DhcpClient) bool {
// TestAddDhcpClient tests the AddDhcpClient method.
func TestAddDhcpClient(t *testing.T) {
client := DhcpClient{
MacAddr: MustParseMAC("AA:BB:CC:DD:EE:FF"),
Hostname: "test-host",
HasStaticIP: true,
FriendlyName: "Test Client",
LastSeen: time.Now(),
MacAddr: MustParseMAC("AA:BB:CC:DD:EE:FF"),
Hostname: "test-host",
//HasStaticIP: true,
//FriendlyName: "Test Client",
LastSeen: time.Now(),
}

db := NewTestDBWithData([]DhcpClient{client})
Expand All @@ -68,8 +68,8 @@ func TestAddDhcpClient(t *testing.T) {
// Compare the inserted and retrieved client using assertions
assert.Equal(t, client.MacAddr, retrievedClient.MacAddr, "MacAddr mismatch")
assert.Equal(t, client.Hostname, retrievedClient.Hostname, "Hostname mismatch")
assert.Equal(t, client.HasStaticIP, retrievedClient.HasStaticIP, "HasStaticIP mismatch")
assert.Equal(t, client.FriendlyName, retrievedClient.FriendlyName, "FriendlyName mismatch")
//assert.Equal(t, client.HasStaticIP, retrievedClient.HasStaticIP, "HasStaticIP mismatch")
//assert.Equal(t, client.FriendlyName, retrievedClient.FriendlyName, "FriendlyName mismatch")

// Allow for slight differences in time, but the retrieved and original times should be very close
assert.WithinDuration(t, client.LastSeen, retrievedClient.LastSeen, time.Second, "LastSeen timestamp mismatch")
Expand All @@ -78,11 +78,11 @@ func TestAddDhcpClient(t *testing.T) {
// TestGetDhcpClient tests the GetDhcpClient method.
func TestGetDhcpClient(t *testing.T) {
client := DhcpClient{
MacAddr: MustParseMAC("AA:BB:CC:DD:EE:FF"),
Hostname: "test-host",
HasStaticIP: true,
FriendlyName: "Test Client",
LastSeen: time.Now(),
MacAddr: MustParseMAC("AA:BB:CC:DD:EE:FF"),
Hostname: "test-host",
//HasStaticIP: true,
//FriendlyName: "Test Client",
LastSeen: time.Now(),
}

db := NewTestDBWithData([]DhcpClient{client})
Expand All @@ -94,8 +94,8 @@ func TestGetDhcpClient(t *testing.T) {
// Check the values of the retrieved client using assertions
assert.Equal(t, client.MacAddr, retrievedClient.MacAddr, "MacAddr mismatch")
assert.Equal(t, client.Hostname, retrievedClient.Hostname, "Hostname mismatch")
assert.Equal(t, client.HasStaticIP, retrievedClient.HasStaticIP, "HasStaticIP mismatch")
assert.Equal(t, client.FriendlyName, retrievedClient.FriendlyName, "FriendlyName mismatch")
//assert.Equal(t, client.HasStaticIP, retrievedClient.HasStaticIP, "HasStaticIP mismatch")
//assert.Equal(t, client.FriendlyName, retrievedClient.FriendlyName, "FriendlyName mismatch")
assert.WithinDuration(t, client.LastSeen, retrievedClient.LastSeen, time.Second, "LastSeen timestamp mismatch")

// Test retrieving a non-existent client
Expand All @@ -110,25 +110,25 @@ func TestGetDeadDhcpClients(t *testing.T) {
// Create some test DHCP clients in the database
clientsInDB := []DhcpClient{
{
MacAddr: MustParseMAC("AA:BB:CC:DD:EE:FF"),
Hostname: "test-host-1",
HasStaticIP: true,
FriendlyName: "Test Client 1",
LastSeen: timeNow,
MacAddr: MustParseMAC("AA:BB:CC:DD:EE:FF"),
Hostname: "test-host-1",
//HasStaticIP: true,
//FriendlyName: "Test Client 1",
LastSeen: timeNow,
},
{
MacAddr: MustParseMAC("11:22:33:44:55:66"),
Hostname: "test-host-2",
HasStaticIP: false,
FriendlyName: "Test Client 2",
LastSeen: timeNow,
MacAddr: MustParseMAC("11:22:33:44:55:66"),
Hostname: "test-host-2",
//HasStaticIP: false,
//FriendlyName: "Test Client 2",
LastSeen: timeNow,
},
{
MacAddr: MustParseMAC("77:88:99:AA:BB:CC"),
Hostname: "test-host-3",
HasStaticIP: true,
FriendlyName: "Test Client 3",
LastSeen: timeNow,
MacAddr: MustParseMAC("77:88:99:AA:BB:CC"),
Hostname: "test-host-3",
//HasStaticIP: true,
//FriendlyName: "Test Client 3",
LastSeen: timeNow,
},
}

Expand Down
38 changes: 38 additions & 0 deletions dhcp-clients-webapp-backend/pkg/trackerdb/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package trackerdb

import (
"encoding/json"
"net"
"time"

// import sqlite3 driver, so that database/sql package will know how to deal with "sqlite3" type
_ "github.com/mattn/go-sqlite3"
)

// DhcpClient represents the structure for a DHCP client.
// The DHCP client might be currently connected to the server or not; in other words this
// may represent a DHCP client that has been connected in the past, but currently is not.
type DhcpClient struct {
MacAddr net.HardwareAddr
Hostname string
HasStaticIP bool
FriendlyName string
LastSeen time.Time
}

// MarshalJSON customizes the JSON serialization for DhcpClientData
func (d DhcpClient) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
MacAddr string `json:"mac_addr"`
Hostname string `json:"hostname"`
HasStaticIP bool `json:"has_static_ip"`
FriendlyName string `json:"friendly_name"`
LastSeen int64 `json:"last_seen"`
}{
MacAddr: d.MacAddr.String(),
Hostname: d.Hostname,
HasStaticIP: d.HasStaticIP,
FriendlyName: d.FriendlyName,
LastSeen: d.LastSeen.Unix(),
})
}
8 changes: 7 additions & 1 deletion dhcp-clients-webapp-backend/pkg/uibackend/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ type AddonConfig struct {
WebUIPort int `json:"web_ui_port"`
}

// WebSocket
// WebSocketMessage defines which contents get transmitted over the websocket in the
// BACKEND -> UI direction.
// Any structure contained here should have a sensible JSON marshalling helper.
type WebSocketMessage struct {
// CurrentClients contains the list of clients currently "connected" to the dnsmasq server.
// In this context "connected" means: that sent DHCP traffic since the dnsmasq server was started.
Expand All @@ -106,4 +108,8 @@ type WebSocketMessage struct {
// PastClients contains the list of clients that were connected in the past, but never
// obtained a DHCP lease since the last dnsmasq server restart.
PastClients []trackerdb.DhcpClient `json:"past_clients"`

// DHCPServerStartTime indicates the point in time where
// the dnsmasq server was last started, expressed as Unix epoch.
DHCPServerStartTime int64 `json:"dhcp_server_starttime"`
}
19 changes: 13 additions & 6 deletions dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,19 @@ type UIBackend struct {
dhcpStartIP net.IP
dhcpEndIP net.IP

// time this application was started
startTimestamp time.Time

// the actual HTTP server
serverPort int
server http.Server
upgrader websocket.Upgrader

// more HTTP server resources
// the CSS template as read from file
isTestingMode bool
htmlTemplate *template.Template
jsContents string
cssContents string
htmlTemplate *template.Template // read from disk at startup
jsContents string // read from disk at startup
cssContents string // read from disk at startup

// map of connected websockets
clients map[*websocket.Conn]bool
Expand All @@ -60,7 +62,7 @@ type UIBackend struct {
dhcpClientData []DhcpClientData
dhcpClientDataLock sync.Mutex

// DB tracking all DHCP clients
// DB tracking all DHCP clients, used to provide the "past DHCP clients" feature
trackerDB trackerdb.DhcpClientTrackerDB

// channel used to broadcast tabular data from backend->frontend
Expand All @@ -82,6 +84,7 @@ func NewUIBackend() UIBackend {
isTestingMode := os.Getenv("LOCAL_TESTING") != ""

return UIBackend{
startTimestamp: time.Now(),
ipAddressReservations: make(map[netip.Addr]IpAddressReservation),
friendlyNames: make(map[string]DhcpClientFriendlyName),
clients: make(map[*websocket.Conn]bool),
Expand Down Expand Up @@ -148,10 +151,14 @@ func (b *UIBackend) getWebSocketMessage() WebSocketMessage {
log.Default().Printf("ERR: failed to get list of dead/past DHCP clients: %s", err.Error())
// keep going
}

return WebSocketMessage{
CurrentClients: currentClients,
PastClients: deadClients,

// we approximate the DHCP server start time with this app's start time;
// the reason is that inside the HA addon, dnsmasq is started at about the same
// time of this app
DHCPServerStartTime: b.startTimestamp.Unix(),
}
}

Expand Down
Loading

0 comments on commit 943e460

Please sign in to comment.