From 7a72c6a5ab042c35d8562fdb165763f46888dee3 Mon Sep 17 00:00:00 2001 From: Francesco Montorsi Date: Tue, 29 Oct 2024 10:32:33 +0100 Subject: [PATCH] New feature: associate a link with each DHCP client (#16) This PR is adding a new column "Link" to the Current DHCP clients table, which contains the rendering of a new (optional) "link" property associated to each IP address reservation or DHCP friendly name in the configuration file. --- config.yaml | 14 ++- .../pkg/uibackend/types.go | 49 +++++++++-- .../pkg/uibackend/uibackend.go | 57 +++++++++++- .../pkg/uibackend/uibackend_test.go | 86 +++++++++++++++++-- .../templates/dnsmasq-dhcp.js | 14 ++- .../templates/scss/style.scss | 13 +++ .../templates/style.css | 10 +++ .../templates/style.css.map | 2 +- rootfs/opt/bin/dnsmasq-dhcp-script.sh | 2 +- test-leases.leases | 4 +- test-options.json | 15 ++-- 11 files changed, 234 insertions(+), 32 deletions(-) diff --git a/config.yaml b/config.yaml index 910f147..7ab058f 100644 --- a/config.yaml +++ b/config.yaml @@ -3,9 +3,9 @@ # as reported by the Github Action workflow 'publish.yaml', so that you can force HomeAssistant # to use the docker image of that feature branch instead of the docker image of 'main', by pointing # HomeAssistant to that feature branch -version: 1.4.1 -slug: dnsmasq-dhcp -name: Dnsmasq-DHCP +version: beta +slug: dnsmasq-dhcp-beta +name: Dnsmasq-DHCP BETA description: A DHCP server based on dnsmasq url: https://github.com/f18m/ha-addon-dnsmasq-dhcp-server/tree/main advanced: true @@ -56,9 +56,15 @@ options: - mac: aa:bb:cc:dd:ee:ff name: "An-important-host-with-reserved-IP" ip: 192.168.1.15 + # the 'link' property accepts a basic golang template. Available variables are 'mac', 'name' and 'ip' + # e.g. "http://{{ ip }}/landing/page" + link: dhcp_clients_friendly_names: - mac: dd:ee:aa:dd:bb:ee name: "This is a friendly name to label this host, even if it gets a dynamic IP" + # the 'link' property accepts a basic golang template. Available variables are 'mac', 'name' and 'ip' + # e.g. "http://{{ ip }}/landing/page/for/this/dynamic/host" + link: log_dhcp: true log_web_ui: false # this addon uses "host_network: true" so the internal HTTP server will bind on the interface @@ -92,9 +98,11 @@ schema: # the name in this case must be a valid hostname as per RFC 1123 since it is passed to dnsmasq # that will refuse to start if an invalid hostname format is used name: match(^[a-zA-Z0-9\-.]*$) + link: "str?" dhcp_clients_friendly_names: - mac: match(^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$) name: str + link: "str?" log_dhcp: bool log_web_ui: bool web_ui_port: int diff --git a/dhcp-clients-webapp-backend/pkg/uibackend/types.go b/dhcp-clients-webapp-backend/pkg/uibackend/types.go index c1bb2d8..2c54a20 100644 --- a/dhcp-clients-webapp-backend/pkg/uibackend/types.go +++ b/dhcp-clients-webapp-backend/pkg/uibackend/types.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "net/netip" + "text/template" "github.com/b0ch3nski/go-dnsmasq-utils/dnsmasq" ) @@ -31,8 +32,14 @@ type DhcpClientData struct { IsInsideDHCPPool bool // Sometimes the hostname provided by the DHCP client to the DHCP server is really awkward and - // non-informative, so we allow users to override that. + // non-informative, so we allow users to override that from configuration. + // If such an override is available in config, this field gets populated. FriendlyName string + + // In the configuration file it's possible to specify a golang template that is rendered to + // produce a string which is intended to be an URL/URI to show for each DHCP client in the web UI. + // If such link template is available in config, this field gets populated. + EvaluatedLink string } // MarshalJSON customizes the JSON serialization for DhcpClientData @@ -47,6 +54,7 @@ func (d DhcpClientData) MarshalJSON() ([]byte, error) { HasStaticIP bool `json:"has_static_ip"` IsInsideDHCPPool bool `json:"is_inside_dhcp_pool"` FriendlyName string `json:"friendly_name"` + EvaluatedLink string `json:"evaluated_link"` }{ Lease: struct { Expires int64 `json:"expires"` @@ -62,6 +70,7 @@ func (d DhcpClientData) MarshalJSON() ([]byte, error) { HasStaticIP: d.HasStaticIP, IsInsideDHCPPool: d.IsInsideDHCPPool, FriendlyName: d.FriendlyName, + EvaluatedLink: d.EvaluatedLink, }) } @@ -77,13 +86,15 @@ type PastDhcpClientData struct { type DhcpClientFriendlyName struct { MacAddress net.HardwareAddr FriendlyName string + Link template.Template } // IpAddressReservation represents a static IP configuration loaded from the addon configuration file type IpAddressReservation struct { - Name string `json:"name"` - Mac string `json:"mac"` - IP string `json:"ip"` + Name string + Mac net.HardwareAddr + IP netip.Addr + Link template.Template } // AddonConfig is used to unmarshal HomeAssistant option file correctly @@ -121,11 +132,17 @@ func (b *AddonConfig) UnmarshalJSON(data []byte) error { // JSON parse var cfg struct { - IpAddressReservations []IpAddressReservation `json:"ip_address_reservations"` + IpAddressReservations []struct { + Name string `json:"name"` + Mac string `json:"mac"` + IP string `json:"ip"` + Link string `json:"link"` + } `json:"ip_address_reservations"` DhcpClientsFriendlyNames []struct { Name string `json:"name"` Mac string `json:"mac"` + Link string `json:"link"` } `json:"dhcp_clients_friendly_names"` DhcpRange struct { @@ -169,12 +186,24 @@ func (b *AddonConfig) UnmarshalJSON(data []byte) error { return fmt.Errorf("invalid MAC address found inside 'ip_address_reservations': %s", r.Mac) } + linkTemplate, err := template.New("linkTemplate").Parse(r.Link) + if err != nil { + return fmt.Errorf("invalid golang template found inside 'link': %s", r.Link) + } + // normalize the IP and MAC address format (e.g. to lowercase) r.IP = ipAddr.String() r.Mac = macAddr.String() - b.ipAddressReservationsByIP[ipAddr] = r - b.ipAddressReservationsByMAC[macAddr.String()] = r + ipReservation := IpAddressReservation{ + Name: r.Name, + Mac: macAddr, + IP: ipAddr, + Link: *linkTemplate, + } + + b.ipAddressReservationsByIP[ipAddr] = ipReservation + b.ipAddressReservationsByMAC[macAddr.String()] = ipReservation } // convert friendly names to a map of DhcpClientFriendlyName instances indexed by MAC address @@ -184,9 +213,15 @@ func (b *AddonConfig) UnmarshalJSON(data []byte) error { return fmt.Errorf("invalid MAC address found inside 'dhcp_clients_friendly_names': %s", client.Mac) } + linkTemplate, err := template.New("linkTemplate").Parse(client.Link) + if err != nil { + return fmt.Errorf("invalid golang template found inside 'link': %s", client.Link) + } + b.friendlyNames[macAddr.String()] = DhcpClientFriendlyName{ MacAddress: macAddr, FriendlyName: client.Name, + Link: *linkTemplate, } } diff --git a/dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go b/dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go index 91f12ee..6323666 100644 --- a/dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go +++ b/dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go @@ -1,6 +1,7 @@ package uibackend import ( + "bytes" "cmp" "context" "dhcp-clients-webapp-backend/pkg/logger" @@ -12,11 +13,13 @@ import ( "net" "net/http" "net/netip" + "net/url" "os" "slices" "strconv" "strings" "sync" + texttemplate "text/template" "time" "github.com/b0ch3nski/go-dnsmasq-utils/dnsmasq" @@ -469,11 +472,11 @@ func (b *UIBackend) hasIpAddressReservationByIP(ip netip.Addr, macExpected net.H if hasReservation { // the IP address provided is a reserved one... // check if the MAC address is the one for which that IP was intended... - if strings.EqualFold(macExpected.String(), b.cfg.ipAddressReservationsByIP[ip].Mac) { + if strings.EqualFold(macExpected.String(), b.cfg.ipAddressReservationsByIP[ip].Mac.String()) { return true } else { b.logger.Warnf("the IP %s was leased to MAC address %s, but in configuration it was reserved for MAC %s\n", - ip.String(), macExpected.String(), b.cfg.ipAddressReservationsByIP[ip].Mac) + ip.String(), macExpected.String(), b.cfg.ipAddressReservationsByIP[ip].Mac.String()) } } return false @@ -484,6 +487,55 @@ func (b *UIBackend) hasIpAddressReservationByMAC(mac net.HardwareAddr) bool { return hasReservation } +// isValidURI checks if the given string is a valid URI. +func isValidURI(uri string) bool { + parsedURL, err := url.ParseRequestURI(uri) + return err == nil && parsedURL.Scheme != "" && parsedURL.Host != "" +} + +func (b *UIBackend) evaluateLink(hostname string, ip netip.Addr, mac net.HardwareAddr) string { + + var theTemplate *texttemplate.Template + var friendlyName string + + r, hasFriendlyName := b.cfg.friendlyNames[mac.String()] + if hasFriendlyName { + theTemplate = &r.Link + friendlyName = r.FriendlyName + } else { + r, hasReservation := b.cfg.ipAddressReservationsByIP[ip] + if hasReservation { + theTemplate = &r.Link + } + } + + if theTemplate == nil { + return "" + } + + // Create a buffer to capture the output + var buf bytes.Buffer + + // Execute the template with the provided data + err := theTemplate.Execute(&buf, map[string]string{ + "mac": mac.String(), + "ip": ip.String(), + "hostname": hostname, + "friendly_name": friendlyName, + }) + if err != nil { + b.logger.Warnf("failed to render the link template [%v]", theTemplate) + return "" + } + + lnk := buf.String() + if !isValidURI(lnk) { + b.logger.Warnf("rendering [%v] produced an invalid URI [%s]", theTemplate, lnk) + return "" + } + return lnk +} + // Process a slice of dnsmasq.Lease and store that into the UIBackend object func (b *UIBackend) processLeaseUpdatesFromArray(updatedLeases []*dnsmasq.Lease) { @@ -497,6 +549,7 @@ func (b *UIBackend) processLeaseUpdatesFromArray(updatedLeases []*dnsmasq.Lease) d.FriendlyName = b.getFriendlyNameFor(lease.MacAddr, lease.Hostname) d.HasStaticIP = b.hasIpAddressReservationByIP(lease.IPAddr, lease.MacAddr) d.IsInsideDHCPPool = IpInRange(lease.IPAddr, b.cfg.dhcpStartIP, b.cfg.dhcpEndIP) + d.EvaluatedLink = b.evaluateLink(lease.Hostname, lease.IPAddr, lease.MacAddr) // processing complete: b.dhcpClientData = append(b.dhcpClientData, d) diff --git a/dhcp-clients-webapp-backend/pkg/uibackend/uibackend_test.go b/dhcp-clients-webapp-backend/pkg/uibackend/uibackend_test.go index b4b0116..b9bcc34 100644 --- a/dhcp-clients-webapp-backend/pkg/uibackend/uibackend_test.go +++ b/dhcp-clients-webapp-backend/pkg/uibackend/uibackend_test.go @@ -6,6 +6,7 @@ import ( "net" "net/netip" "testing" + "text/template" "github.com/b0ch3nski/go-dnsmasq-utils/dnsmasq" "github.com/google/go-cmp/cmp" @@ -21,59 +22,124 @@ func MustParseMAC(s string) net.HardwareAddr { return mac } -// Test function -func TestProcessLeaseUpdatesFromArray(t *testing.T) { - // Prepare mock leases - leases := []*dnsmasq.Lease{ +func MustParseTemplate(s string) template.Template { + return *template.Must(template.New("test").Parse(s)) +} + +func getMockLeases() []*dnsmasq.Lease { + return []*dnsmasq.Lease{ { + // client1 MacAddr: MustParseMAC("00:11:22:33:44:55"), IPAddr: netip.MustParseAddr("192.168.0.2"), Hostname: "client1", }, { + // client2 MacAddr: MustParseMAC("00:11:22:33:44:56"), IPAddr: netip.MustParseAddr("192.168.0.3"), Hostname: "client2", }, { + // client3 MacAddr: MustParseMAC("00:11:22:33:44:57"), IPAddr: netip.MustParseAddr("192.168.0.101"), Hostname: "client3", }, { + // client4 MacAddr: MustParseMAC("aa:bb:CC:DD:ee:FF"), // mixed case MAC address IPAddr: netip.MustParseAddr("192.168.0.66"), Hostname: "client4", }, } +} - // Prepare UIBackend with mock data +func getMockUIBackend() *UIBackend { + // simulate configurations for: + // * IP address reservations + // * friendly names for dynamic clients + // * DHCP range backendcfg := AddonConfig{ friendlyNames: map[string]DhcpClientFriendlyName{ "00:11:22:33:44:55": { // this is the MAC of 'client1' MacAddress: MustParseMAC("00:11:22:33:44:55"), FriendlyName: "FriendlyClient1", + Link: MustParseTemplate("https://{{ .ip }}/client1-page"), }, "aa:bb:cc:dd:ee:ff": { // this is the MAC of 'client4' MacAddress: MustParseMAC("aa:bb:CC:DD:ee:FF"), FriendlyName: "FriendlyClient4", + Link: MustParseTemplate("https://{{ .hostname }}/client4-page"), }, }, ipAddressReservationsByIP: map[netip.Addr]IpAddressReservation{ netip.MustParseAddr("192.168.0.3"): { Name: "test-friendly-name", - Mac: "00:11:22:33:44:56", // this is the MAC of 'client2' - IP: "192.168.0.3", + Mac: MustParseMAC("00:11:22:33:44:56"), // this is the MAC of 'client2' + IP: netip.MustParseAddr("192.168.0.3"), + Link: MustParseTemplate("https://{{ .ip }}"), }, }, dhcpStartIP: net.IPv4(192, 168, 0, 1), dhcpEndIP: net.IPv4(192, 168, 0, 100), } - backend := &UIBackend{ + return &UIBackend{ logger: logger.NewCustomLogger("unit tests"), cfg: backendcfg, trackerDB: trackerdb.NewTestDB(), } +} + +// TestEvaluateLink tests evaluateLink() helper with valid template data. +func TestEvaluateLink(t *testing.T) { + tests := []struct { + name string + hostname string + ip netip.Addr + mac net.HardwareAddr + expected string + }{ + { + name: "link from IP address reservation", + hostname: "test-friendly-name", + ip: netip.MustParseAddr("192.168.0.3"), + mac: MustParseMAC("00:11:22:33:44:56"), // this is the MAC of 'client2' + expected: "https://192.168.0.3", + }, + { + name: "link from friendly name", + hostname: "FriendlyClient1", + ip: netip.MustParseAddr("192.168.100.200"), // simulate a dynamic IP + mac: MustParseMAC("00:11:22:33:44:55"), // this is the MAC of 'client1' + expected: "https://192.168.100.200/client1-page", + }, + { + name: "link from friendly name", + hostname: "FriendlyClient4", + ip: netip.MustParseAddr("192.168.100.200"), // simulate a dynamic IP + mac: MustParseMAC("aa:bb:CC:DD:ee:FF"), // this is the MAC of 'client4' + expected: "https://FriendlyClient4/client4-page", + }, + } + + backend := getMockUIBackend() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := backend.evaluateLink(tt.hostname, tt.ip, tt.mac) + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +} + +// Test function +func TestProcessLeaseUpdatesFromArray(t *testing.T) { + // Prepare mock data + leases := getMockLeases() + backend := getMockUIBackend() // Call the method being tested backend.processLeaseUpdatesFromArray(leases) @@ -90,6 +156,7 @@ func TestProcessLeaseUpdatesFromArray(t *testing.T) { FriendlyName: "FriendlyClient1", // check friendly name has been associated successfully HasStaticIP: false, IsInsideDHCPPool: true, + EvaluatedLink: "https://192.168.0.2/client1-page", }, { Lease: dnsmasq.Lease{ @@ -100,6 +167,7 @@ func TestProcessLeaseUpdatesFromArray(t *testing.T) { FriendlyName: "client2", HasStaticIP: true, // check the IP address reservation has been recognized successfully IsInsideDHCPPool: true, + EvaluatedLink: "https://192.168.0.3", }, { Lease: dnsmasq.Lease{ @@ -110,6 +178,7 @@ func TestProcessLeaseUpdatesFromArray(t *testing.T) { FriendlyName: "FriendlyClient4", HasStaticIP: false, IsInsideDHCPPool: true, + EvaluatedLink: "https://client4/client4-page", }, { Lease: dnsmasq.Lease{ @@ -120,6 +189,7 @@ func TestProcessLeaseUpdatesFromArray(t *testing.T) { FriendlyName: "client3", HasStaticIP: false, IsInsideDHCPPool: false, // check if the condition "outside DHCP pool" has been recognized successfully + EvaluatedLink: "", // no "link" can be rendered since this client is not in configuration }, } diff --git a/dhcp-clients-webapp-backend/templates/dnsmasq-dhcp.js b/dhcp-clients-webapp-backend/templates/dnsmasq-dhcp.js index 00a12fa..b0fc937 100644 --- a/dhcp-clients-webapp-backend/templates/dnsmasq-dhcp.js +++ b/dhcp-clients-webapp-backend/templates/dnsmasq-dhcp.js @@ -100,6 +100,7 @@ function initCurrentTable() { { title: '#', type: 'num' }, { title: 'Friendly Name', type: 'string' }, { title: 'Hostname', type: 'string' }, + { title: 'Link', type: 'string' }, { title: 'IP Address', type: 'ip-address' }, { title: 'MAC Address', type: 'string' }, { title: 'Expires in', 'orderDataType': 'custom-date-order' }, @@ -194,10 +195,19 @@ function processWebSocketEvent(event) { dhcp_static_ip += 1; } + external_link_symbol="🡕" + //external_link_symbol="⧉" + if (item.evaluated_link) { + link_str = "" + item.evaluated_link + " " + external_link_symbol + } else { + link_str = "N/A" + } + // append new row tableData.push([index + 1, - item.friendly_name, item.lease.hostname, item.lease.ip_addr, - item.lease.mac_addr, formatTimeLeft(item.lease.expires), static_ip_str]); + item.friendly_name, item.lease.hostname, link_str, + item.lease.ip_addr, item.lease.mac_addr, + formatTimeLeft(item.lease.expires), static_ip_str]); }); table_current.clear().rows.add(tableData).draw(false /* do not reset page position */); diff --git a/dhcp-clients-webapp-backend/templates/scss/style.scss b/dhcp-clients-webapp-backend/templates/scss/style.scss index 0bed54e..009d42d 100644 --- a/dhcp-clients-webapp-backend/templates/scss/style.scss +++ b/dhcp-clients-webapp-backend/templates/scss/style.scss @@ -39,6 +39,19 @@ $backgroundMonoText: lightgrey; //display: flex; } +/* +a[href*="http"]::after { + content: '⎋'; + display: inline-block; + transform: rotate(90deg); + margin-inline-start: var(--space1); + vertical-align: middle; + font-size: 0.75em; +}*/ + + +// tabs implementations + .btn { font-size: 12px; font-weight: 600; diff --git a/dhcp-clients-webapp-backend/templates/style.css b/dhcp-clients-webapp-backend/templates/style.css index b39c749..dbe5a4a 100644 --- a/dhcp-clients-webapp-backend/templates/style.css +++ b/dhcp-clients-webapp-backend/templates/style.css @@ -1,3 +1,4 @@ +@charset "UTF-8"; /* SCSS style sheet for HA dnsmasq-dhcp addon @@ -29,6 +30,15 @@ margin: 0 auto; } +/* +a[href*="http"]::after { + content: '⎋'; + display: inline-block; + transform: rotate(90deg); + margin-inline-start: var(--space1); + vertical-align: middle; + font-size: 0.75em; +}*/ .btn { font-size: 12px; font-weight: 600; diff --git a/dhcp-clients-webapp-backend/templates/style.css.map b/dhcp-clients-webapp-backend/templates/style.css.map index 2d215b7..3a84927 100644 --- a/dhcp-clients-webapp-backend/templates/style.css.map +++ b/dhcp-clients-webapp-backend/templates/style.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../sass/style.scss"],"names":[],"mappings":"AAAA;AAAA;;AAAA;AAAA;AAYA;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA,kBAZiB;EAajB;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EAEI;EACA;;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA,OAzCc;EA0Cd;;AACA;EATJ;IAUQ;;;AAGJ;EACI,OA9CQ;EA+CR;;AAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKR;EACI,kBA/DY;EAgEZ;EACA;;AAEA;EAEI;;AAGJ;EACI;;AAGA;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EALJ;IAMQ;;;AAGJ;EACI;EACA;;AAKJ;EACI;EACA;;AAGJ;EACI;;AAMZ;EACI","file":"style.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../sass/style.scss"],"names":[],"mappings":";AAAA;AAAA;;AAAA;AAAA;AAYA;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA,kBAZiB;EAajB;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EAEI;EACA;;;AAKJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA;EACI;EACA;EACA;EACA;EACA;EACA;EACA,OAtDc;EAuDd;;AACA;EATJ;IAUQ;;;AAGJ;EACI,OA3DQ;EA4DR;;AAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKR;EACI,kBA5EY;EA6EZ;EACA;;AAEA;EAEI;;AAGJ;EACI;;AAGA;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAEA;EALJ;IAMQ;;;AAGJ;EACI;EACA;;AAKJ;EACI;EACA;;AAGJ;EACI;;AAMZ;EACI","file":"style.css"} \ No newline at end of file diff --git a/rootfs/opt/bin/dnsmasq-dhcp-script.sh b/rootfs/opt/bin/dnsmasq-dhcp-script.sh index d3c53c8..d538c4d 100755 --- a/rootfs/opt/bin/dnsmasq-dhcp-script.sh +++ b/rootfs/opt/bin/dnsmasq-dhcp-script.sh @@ -88,7 +88,7 @@ ON CONFLICT(mac_addr) DO UPDATE SET EOF if [[ $? -eq 0 ]]; then - log_info "Stored in trackerDB updated information for client mac=$mac_addr, hostname=$hostname: last_seen=$last_seen, Ddhcp_server_start_epoch=$dhcp_server_start_counter" + log_info "Stored in trackerDB updated information for client mac=$mac_addr, hostname=$hostname: last_seen=$last_seen, dhcp_server_start_epoch=$dhcp_server_start_counter" else log_error "Failed to add/update client. Expect inconsistencies." fi diff --git a/test-leases.leases b/test-leases.leases index 163cc1b..394d009 100644 --- a/test-leases.leases +++ b/test-leases.leases @@ -1,5 +1,5 @@ -1727948636 f8:25:51:d9:1e:73 192.168.1.21 host-a 01:f8:25:51:d9:1e:73 -1727948544 08:a6:f7:4f:13:04 192.168.1.47 host-b 01:08:a6:f7:4f:13:04 +1727948636 aa:bb:cc:dd:ee:00 192.168.1.15 static-ip-important-host 01:aa:bb:cc:dd:ee:00 +1727948544 aa:bb:cc:dd:ee:01 192.168.1.55 static-ip-within-dhcp-range aa:bb:cc:dd:ee:01 1727948521 40:ae:30:50:15:cb 192.168.1.41 host-c * 1727948425 80:64:6f:dc:73:b8 192.168.1.44 host-d 01:80:64:6f:dc:73:b8 1727948248 80:64:6f:c8:1f:3c 192.168.1.43 host-e 01:80:64:6f:c8:1f:3c diff --git a/test-options.json b/test-options.json index a6a4410..0126460 100644 --- a/test-options.json +++ b/test-options.json @@ -4,19 +4,22 @@ "ip_address_reservations": [ { "mac": "aa:bb:cc:dd:ee:00", - "name": "an-important-host-with-static-ip", - "ip": "192.168.1.15" + "name": "static-ip-important-host", + "ip": "192.168.1.15", + "link": "https://{{ .ip }}/important-host-land-page" }, { "mac": "aa:bb:cc:dd:ee:01", - "name": "an-host-within-dhcp-range", - "ip": "192.168.1.55" + "name": "static-ip-within-dhcp-range", + "ip": "192.168.1.55", + "link": "https://{{ .ip }}/less-important" } ], "dhcp_clients_friendly_names": [ { - "name": "a human-friendly name", - "mac": "4c:50:77:cf:3c:35" + "name": "a human-friendly name for a DHCP client with dynamic IP", + "mac": "4c:50:77:cf:3c:35", + "link": "https://{{ .ip }}/page-{{ .hostname }}" } ], "dhcp_range": {