Skip to content

Commit

Permalink
Introduce new onboarding strategy (#196)
Browse files Browse the repository at this point in the history
* Update the Readme to match the new strategy

* Implement new onboarding strategy

* Fix linting issue

* Refactor config loading

* Use switch when determining the onboarding strategy

---------

Co-authored-by: Andreas Fritzler <[email protected]>
  • Loading branch information
damyan and afritzler authored Oct 25, 2024
1 parent 04225e6 commit eabd532
Show file tree
Hide file tree
Showing 5 changed files with 415 additions and 79 deletions.
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,29 @@ As for in-band, a kubernetes namespace shall be passed as a parameter. Further,
The Metal plugin acts as a connection link between DHCP and the IronCore metal stack. It creates an `EndPoint` object for each machine with leased IP address. Those endpoints are then consumed by the metal operator, who then creates the corresponding `Machine` objects.

### Configuration
Path to an inventory yaml shall be passed as a string. It represents a list of machines as follows:
Path to an inventory yaml shall be passed as a string. Currently, there are two different ways to provide an inventory list: either by specifying a MAC address filter or by providing the inventory list explicitly. If both a static list and a filter are specified in the `inventory.yaml`, the static list gets a precedence, so the filter will be ignored.

Providing an explicit static inventory list in `inventory.yaml` goes as follows:
```yaml
- name: server-01
macAddress: 00:1A:2B:3C:4D:5E
- name: server-02
macAddress: 00:1A:2B:3C:4D:5F
hosts:
- name: server-01
macAddress: 00:1A:2B:3C:4D:5E
- name: server-02
macAddress: 00:1A:2B:3C:4D:5F
```
Providing a MAC address prefix filter list creates `Endpoint`s with a predefined prefix name. When the MAC address of an inventory does not match the prefix, the inventory will not be onboarded, so for now no "onboarding by default" occurs. Obviously a full MAC address is a valid prefix filter.
To get inventories with certain MACs onboarded, the following `inventory.yaml` shall be specified:
```yaml
namePrefix: server- # optional prefix, default: "compute-"
filter:
macPrefix:
- 00:1A:2B:3C:4D:5E
- 00:1A:2B:3C:4D:5F
- 00:AA:BB
```
The inventories above will get auto-generated names like `server-aybz`.

### Notes
- supports both IPv4 and IPv6
- IPv6 relays are supported, IPv4 are not
Expand Down
10 changes: 10 additions & 0 deletions internal/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@ type Inventory struct {
Name string `yaml:"name"`
MacAddress string `yaml:"macAddress"`
}

type Filter struct {
MacPrefix []string `yaml:"macPrefix"`
}

type Config struct {
NamePrefix string `yaml:"namePrefix"`
Inventories []Inventory `yaml:"hosts"`
Filter Filter `yaml:"filter"`
}
198 changes: 161 additions & 37 deletions plugins/metal/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"os"
"strings"

"sigs.k8s.io/controller-runtime/pkg/client"

"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

"github.com/coredhcp/coredhcp/handler"
Expand All @@ -36,9 +38,24 @@ var Plugin = plugins.Plugin{
}

// map MAC address to inventory name
var inventoryMap map[string]string
var inventory *Inventory

type Inventory struct {
Entries map[string]string
Strategy OnBoardingStrategy
}

// default inventory name prefix
const defaultNamePrefix = "compute-"

type OnBoardingStrategy string

// args[0] = path to configuration file
const (
OnBoardingStrategyStatic OnBoardingStrategy = "Static"
OnboardingStrategyDynamic OnBoardingStrategy = "Dynamic"
)

// args[0] = path to inventory file
func parseArgs(args ...string) (string, error) {
if len(args) != 1 {
return "", fmt.Errorf("exactly one argument must be passed to the metal plugin, got %d", len(args))
Expand All @@ -48,48 +65,76 @@ func parseArgs(args ...string) (string, error) {

func setup6(args ...string) (handler.Handler6, error) {
var err error
inventoryMap, err = loadConfig(args...)
inventory, err = loadConfig(args...)
if err != nil {
return nil, err
}
if inventory == nil || len(inventory.Entries) == 0 {
return nil, nil
}

return handler6, nil
}

func loadConfig(args ...string) (map[string]string, error) {
func loadConfig(args ...string) (*Inventory, error) {
path, err := parseArgs(args...)
if err != nil {
return nil, fmt.Errorf("invalid configuration: %v", err)
}

log.Infof("Reading metal config file %s", path)
log.Debugf("Reading metal config file %s", path)
configData, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %v", err)
}

var config []api.Inventory
var config api.Config
if err = yaml.Unmarshal(configData, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %v", err)
}

inventories := make(map[string]string)
for _, i := range config {
if i.MacAddress != "" && i.Name != "" {
inventories[strings.ToLower(i.MacAddress)] = i.Name
inv := &Inventory{}
entries := make(map[string]string)
switch {
// static inventory list has precedence, always
case len(config.Inventories) > 0:
inv.Strategy = OnBoardingStrategyStatic
log.Debug("Using static list onboarding")
for _, i := range config.Inventories {
if i.MacAddress != "" && i.Name != "" {
entries[strings.ToLower(i.MacAddress)] = i.Name
}
}
case len(config.Filter.MacPrefix) > 0:
inv.Strategy = OnboardingStrategyDynamic
namePrefix := defaultNamePrefix
if config.NamePrefix != "" {
namePrefix = config.NamePrefix
}
log.Debugf("Using MAC address prefix filter onboarding with name prefix '%s'", namePrefix)
for _, i := range config.Filter.MacPrefix {
entries[strings.ToLower(i)] = namePrefix
}
default:
log.Infof("No inventories loaded")
return nil, nil
}

log.Infof("Loaded metal config with %d inventories", len(inventories))
return inventories, nil
inv.Entries = entries

log.Infof("Loaded metal config with %d inventories", len(entries))
return inv, nil
}

func setup4(args ...string) (handler.Handler4, error) {
var err error
inventoryMap, err = loadConfig(args...)
inventory, err = loadConfig(args...)
if err != nil {
return nil, err
}
if inventory == nil || len(inventory.Entries) == 0 {
return nil, nil
}

return handler4, nil
}
Expand All @@ -109,7 +154,7 @@ func handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
return nil, true
}

if err := applyEndpointForMACAddress(mac, ipamv1alpha1.CIPv6SubnetType); err != nil {
if err := ApplyEndpointForMACAddress(mac, ipamv1alpha1.CIPv6SubnetType); err != nil {
log.Errorf("Could not apply endpoint for mac %s: %s", mac.String(), err)
return resp, false
}
Expand All @@ -123,7 +168,7 @@ func handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {

mac := req.ClientHWAddr

if err := applyEndpointForMACAddress(mac, ipamv1alpha1.CIPv4SubnetType); err != nil {
if err := ApplyEndpointForMACAddress(mac, ipamv1alpha1.CIPv4SubnetType); err != nil {
log.Errorf("Could not apply peer address: %s", err)
return resp, false
}
Expand All @@ -132,27 +177,26 @@ func handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
return resp, false
}

func applyEndpointForMACAddress(mac net.HardwareAddr, subnetFamily ipamv1alpha1.SubnetAddressType) error {
inventoryName, ok := inventoryMap[strings.ToLower(mac.String())]
if !ok {
// done here, return no error, next plugin
log.Printf("Unknown inventory MAC address: %s", mac.String())
func ApplyEndpointForMACAddress(mac net.HardwareAddr, subnetFamily ipamv1alpha1.SubnetAddressType) error {
inventoryName := GetInventoryEntryMatchingMACAddress(mac)
if inventoryName == "" {
log.Print("Unknown inventory, not processing")
return nil
}

ip, err := GetIPForMACAddress(mac, subnetFamily)
ip, err := GetIPAMIPAddressForMACAddress(mac, subnetFamily)
if err != nil {
return fmt.Errorf("could not get IP for MAC address %s: %s", mac.String(), err)
return fmt.Errorf("could not get IPAM IP for MAC address %s: %s", mac.String(), err)
}

if ip != nil {
if err := ApplyEndpointForInventory(inventoryName, mac, ip); err != nil {
return fmt.Errorf("could not apply endpoint for inventory: %s", err)
} else {
log.Infof("Successfully applied endpoint for inventory %s[%s]", inventoryName, mac.String())
log.Infof("Successfully applied endpoint for inventory %s (%s)", inventoryName, mac.String())
}
} else {
log.Infof("Could not find IP for MAC address %s", mac.String())
log.Infof("Could not find IPAM IP for MAC address %s", mac.String())
}

return nil
Expand All @@ -167,29 +211,109 @@ func ApplyEndpointForInventory(name string, mac net.HardwareAddr, ip *netip.Addr
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

endpoint := &metalv1alpha1.Endpoint{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: metalv1alpha1.EndpointSpec{
MACAddress: mac.String(),
IP: metalv1alpha1.MustParseIP(ip.String()),
},
}

cl := kubernetes.GetClient()
if cl == nil {
return fmt.Errorf("kubernetes client not initialized")
}

if _, err := controllerutil.CreateOrPatch(ctx, cl, endpoint, nil); err != nil {
return fmt.Errorf("failed to apply endpoint: %v", err)
switch inventory.Strategy {
case OnBoardingStrategyStatic:
// we do know the real name, so CreateOrPatch is fine
endpoint := &metalv1alpha1.Endpoint{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: metalv1alpha1.EndpointSpec{
MACAddress: mac.String(),
IP: metalv1alpha1.MustParseIP(ip.String()),
},
}
if _, err := controllerutil.CreateOrPatch(ctx, cl, endpoint, nil); err != nil {
return fmt.Errorf("failed to apply endpoint: %v", err)
}
case OnboardingStrategyDynamic:
// the (generated) name is unknown, so go for filtering
if existingEndpoint, _ := GetEndpointForMACAddress(mac); existingEndpoint != nil {
if existingEndpoint.Spec.IP.String() != ip.String() {
log.Debugf("Endpoint exists with different IP address, updating IP address %s to %s",
existingEndpoint.Spec.IP.String(), ip.String())

existingEndpointBase := existingEndpoint.DeepCopy()
existingEndpoint.Spec.IP = metalv1alpha1.MustParseIP(ip.String())

if err := cl.Patch(ctx, existingEndpoint, client.MergeFrom(existingEndpointBase)); err != nil {
return fmt.Errorf("failed to patch endpoint: %v", err)
}
}
log.Debugf("Endpoint %s (%s) exists, nothing to do", mac.String(), ip.String())
} else {
log.Debugf("Endpoint %s (%s) does not exist, creating", mac.String(), ip.String())
endpoint := &metalv1alpha1.Endpoint{
ObjectMeta: metav1.ObjectMeta{
GenerateName: name,
},
Spec: metalv1alpha1.EndpointSpec{
MACAddress: mac.String(),
IP: metalv1alpha1.MustParseIP(ip.String()),
},
}
if err := cl.Create(ctx, endpoint); err != nil {
return fmt.Errorf("failed to create endpoint: %v", err)
}
}
default:
return fmt.Errorf("unknown OnboardingStrategy %s", inventory.Strategy)
}

return nil
}

func GetIPForMACAddress(mac net.HardwareAddr, subnetFamily ipamv1alpha1.SubnetAddressType) (*netip.Addr, error) {
func GetEndpointForMACAddress(mac net.HardwareAddr) (*metalv1alpha1.Endpoint, error) {
cl := kubernetes.GetClient()
if cl == nil {
return nil, fmt.Errorf("kubernetes client not initialized")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

epList := &metalv1alpha1.EndpointList{}
if err := cl.List(ctx, epList); err != nil {
return nil, fmt.Errorf("failed to list Endpoints: %v", err)
}

for _, ep := range epList.Items {
if ep.Spec.MACAddress == mac.String() {
return &ep, nil
}
}
return nil, nil
}

func GetInventoryEntryMatchingMACAddress(mac net.HardwareAddr) string {
switch inventory.Strategy {
case OnBoardingStrategyStatic:
inventoryName, ok := inventory.Entries[strings.ToLower(mac.String())]
if !ok {
log.Debugf("Unknown inventory MAC address: %s", mac.String())
} else {
return inventoryName
}
case OnboardingStrategyDynamic:
for i := range inventory.Entries {
if strings.HasPrefix(strings.ToLower(mac.String()), strings.ToLower(i)) {
return inventory.Entries[i]
}
}
// we don't onboard by default yet, might change in the future
log.Debugf("Inventory MAC address %s does not match any inventory MAC prefix", mac.String())
default:
log.Debugf("Unknown Onboarding strategy %s", inventory.Strategy)
}

return ""
}

func GetIPAMIPAddressForMACAddress(mac net.HardwareAddr, subnetFamily ipamv1alpha1.SubnetAddressType) (*netip.Addr, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Expand Down
Loading

0 comments on commit eabd532

Please sign in to comment.