Skip to content

Commit

Permalink
feat: implement autodiscovery.
Browse files Browse the repository at this point in the history
  • Loading branch information
douglasmakey committed Jun 4, 2024
1 parent 41c6be6 commit dda8096
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 7 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ Smee's DHCP functionality can operate in one of the following modes:
Smee will not respond to DHCP requests from clients. This is useful when the network has an existing DHCP server that will provide both IP and next boot info and Smee's TFTP and HTTP functionality will be used. The IP address in the hardware record must be the same as the IP address of the client requesting the `auto.ipxe` script. See this [doc](docs/DHCP.md) for more details.
To enable this mode set `-dhcp-enabled=false`.

### Auto discovery

In proxy DHCP mode, Smee can optionally be configured to provide network boot options to all network boot clients. This is called auto-discovery or auto-enrollment. When set to true, all machines are treated as iPXE machines, and a new hardware entry is automatically created for each when Smee receives a DHCP request. This feature is useful for environments where hardware is frequently added or removed, as it allows for automatic tracking and management of hardware resources.

### Interoperability with other DHCP servers

It is not recommended, but it is possible for Smee to be run in `reservation` mode in networks with another DHCP server(s). To get the intended behavior from Smee one of the following must be true.
Expand Down
1 change: 1 addition & 0 deletions cmd/smee/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func ipxeHTTPScriptFlags(c *config, fs *flag.FlagSet) {
func dhcpFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.dhcp.enabled, "dhcp-enabled", true, "[dhcp] enable DHCP server")
fs.StringVar(&c.dhcp.mode, "dhcp-mode", "reservation", "[dhcp] DHCP mode (reservation, proxy)")
fs.BoolVar(&c.dhcp.autoDiscovery, "dhcp-auto-discovery", false, "[dhcp] enable auto discovery mode (only available with -dhcp-mode=proxy)")
fs.StringVar(&c.dhcp.bindAddr, "dhcp-addr", "0.0.0.0:67", "[dhcp] local IP:Port to listen on for DHCP requests")
fs.StringVar(&c.dhcp.bindInterface, "dhcp-iface", "", "[dhcp] interface to bind to for DHCP requests")
fs.StringVar(&c.dhcp.ipForPacket, "dhcp-ip-for-packet", detectPublicIPv4(""), "[dhcp] IP address to use in DHCP packets (opt 54, etc)")
Expand Down
6 changes: 4 additions & 2 deletions cmd/smee/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type ipxeHTTPScript struct {
type dhcpConfig struct {
enabled bool
mode string
autoDiscovery bool
bindAddr string
bindInterface string
ipForPacket string
Expand Down Expand Up @@ -344,7 +345,7 @@ func (c *config) dhcpHandler(ctx context.Context, log logr.Logger) (server.Handl
return dh, nil
case "proxy":
dh := &proxy.Handler{
Backend: backend,
Backend: backend.(handler.BackendReadWriter),
IPAddr: pktIP,
Log: log,
Netboot: proxy.Netboot{
Expand All @@ -353,7 +354,8 @@ func (c *config) dhcpHandler(ctx context.Context, log logr.Logger) (server.Handl
IPXEScriptURL: ipxeScript,
Enabled: true,
},
OTELEnabled: true,
OTELEnabled: true,
AutoDiscoveryEnabled: c.dhcp.autoDiscovery,
}
return dh, nil
}
Expand Down
8 changes: 8 additions & 0 deletions internal/backend/kube/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ type hardwareNotFoundError struct{}
func (hardwareNotFoundError) NotFound() bool { return true }

func (hardwareNotFoundError) Error() string { return "hardware not found" }

func IsHardwareNotFoundError(err error) bool {
if err == nil {
return false
}
e, ok := err.(hardwareNotFoundError)
return ok && e.NotFound()
}
40 changes: 40 additions & 0 deletions internal/backend/kube/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ import (
"net"
"net/netip"
"net/url"
"strings"

"github.com/tinkerbell/smee/internal/dhcp/data"
"github.com/tinkerbell/tink/api/v1alpha1"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/cluster"
)

const tracerName = "github.com/tinkerbell/smee/dhcp"
const tinkerbellNamespace = "tink-system"

// Backend is a backend implementation that uses the Tinkerbell CRDs to get DHCP data.
type Backend struct {
Expand Down Expand Up @@ -182,6 +186,42 @@ func (b *Backend) GetByIP(ctx context.Context, ip net.IP) (*data.DHCP, *data.Net
return d, n, nil
}

func (b *Backend) CreateByMac(ctx context.Context, mac net.HardwareAddr) error {
tracer := otel.Tracer(tracerName)
ctx, span := tracer.Start(ctx, "backend.kube.CreateByMac")
defer span.End()

hardware := &v1alpha1.Hardware{
ObjectMeta: metav1.ObjectMeta{
Name: strings.Replace(mac.String(), ":", "-", -1),
Namespace: tinkerbellNamespace,
Labels: map[string]string{
"created-by": "autodiscovery",
},
},
Spec: v1alpha1.HardwareSpec{
Interfaces: []v1alpha1.Interface{
{
DHCP: &v1alpha1.DHCP{
MAC: mac.String(),
},
Netboot: &v1alpha1.Netboot{
AllowPXE: ptr.To[bool](true),
AllowWorkflow: ptr.To[bool](true),
},
},
},
},
}

if err := b.cluster.GetClient().Create(ctx, hardware); err != nil {
span.SetStatus(codes.Error, err.Error())
return fmt.Errorf("failed to create hardware for (%v): %w", mac, err)
}

return nil
}

// toDHCPData converts a v1alpha1.DHCP to a data.DHCP data structure.
// if required fields are missing, an error is returned.
// Required fields: v1alpha1.Interface.DHCP.MAC, v1alpha1.Interface.DHCP.IP.Address, v1alpha1.Interface.DHCP.IP.Netmask.
Expand Down
12 changes: 12 additions & 0 deletions internal/dhcp/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,15 @@ type BackendReader interface {
GetByMac(context.Context, net.HardwareAddr) (*data.DHCP, *data.Netboot, error)
GetByIP(context.Context, net.IP) (*data.DHCP, *data.Netboot, error)
}

// BackendWriter is the interface for writing data to a backend.
type BackendWriter interface {
// Write data (to a backend) based on a mac address for creating a new hardware.
CreateByMac(context.Context, net.HardwareAddr) error
}

// BackendReadWriter is the interface for getting and setting data from a backend.
type BackendReadWriter interface {
BackendReader
BackendWriter
}
28 changes: 23 additions & 5 deletions internal/dhcp/handler/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/go-logr/logr"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/tinkerbell/smee/internal/backend/kube"
"github.com/tinkerbell/smee/internal/dhcp"
"github.com/tinkerbell/smee/internal/dhcp/data"
"github.com/tinkerbell/smee/internal/dhcp/handler"
Expand All @@ -39,7 +40,7 @@ const tracerName = "github.com/tinkerbell/smee/internal/dhcp/handler/proxy"
// Handler holds the configuration details for the running the DHCP server.
type Handler struct {
// Backend is the backend to use for getting DHCP data.
Backend handler.BackendReader
Backend handler.BackendReadWriter

// IPAddr is the IP address to use in DHCP responses.
// Option 54 and the sname DHCP header.
Expand All @@ -58,6 +59,9 @@ type Handler struct {
// For example, the filename will be "snp.efi-00-23b1e307bb35484f535a1f772c06910e-d887dc3912240434-01".
// <original filename>-00-<trace id>-<span id>-<trace flags>
OTELEnabled bool

// AutoDiscoveryEnabled is a flag that determines whether a new hardware entry should be created upon receiving a DHCP request.
AutoDiscoveryEnabled bool
}

// Netboot holds the netboot configuration details used in running a DHCP server.
Expand Down Expand Up @@ -184,12 +188,16 @@ func (h *Handler) Handle(ctx context.Context, conn *ipv4.PacketConn, dp data.Pac
// set bootfile header
reply.BootFileName = i.Bootfile("", h.Netboot.IPXEScriptURL(dp.Pkt), h.Netboot.IPXEBinServerHTTP, h.Netboot.IPXEBinServerTFTP)

var notHardwareFound bool
// check the backend, if PXE is NOT allowed, set the boot file name to "/<mac address>/not-allowed"
_, n, err := h.Backend.GetByMac(ctx, dp.Pkt.ClientHWAddr)
if err != nil || (n != nil && !n.AllowNetboot) {
log.V(1).Info("Ignoring packet", "error", err.Error(), "netbootAllowed", n.AllowNetboot)
span.SetStatus(codes.Ok, "netboot not allowed")
return
if err != nil {
notHardwareFound = kube.IsHardwareNotFoundError(err)
if n != nil && !n.AllowNetboot {
log.V(1).Info("Ignoring packet", "error", err.Error(), "netbootAllowed", n.AllowNetboot)
span.SetStatus(codes.Ok, "netboot not allowed")
return
}
}
log.Info(
"received DHCP packet",
Expand Down Expand Up @@ -220,6 +228,16 @@ func (h *Handler) Handle(ctx context.Context, conn *ipv4.PacketConn, dp data.Pac
log.Info("Sent ProxyDHCP response")
span.SetAttributes(h.encodeToAttributes(reply, "reply")...)
span.SetStatus(codes.Ok, "sent DHCP response")

// Create a new hardware entry if AutoDiscoveryEnabled is set to true and a hardware entry does not already exist.
if notHardwareFound && h.AutoDiscoveryEnabled && reply.MessageType() == dhcpv4.MessageTypeAck {
log.Info("AutoDiscoveryEnabled - Creating a new hardware entry", "mac", dp.Pkt.ClientHWAddr.String())
if err := h.Backend.CreateByMac(ctx, dp.Pkt.ClientHWAddr); err != nil {
log.Error(err, "failed to create hardware entry")
span.SetStatus(codes.Error, err.Error())
return
}
}
}

// encodeToAttributes takes a DHCP packet and returns opentelemetry key/value attributes.
Expand Down
1 change: 1 addition & 0 deletions manifests/kustomize/base/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ rules:
- get
- list
- watch
- create
- apiGroups:
- tinkerbell.org
resources:
Expand Down

0 comments on commit dda8096

Please sign in to comment.