Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce new onboarding strategy #196

Merged
merged 5 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
}
197 changes: 160 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,75 @@ 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)
// static inventory list has precedence, always
if len(config.Inventories) > 0 {
damyan marked this conversation as resolved.
Show resolved Hide resolved
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
}
}
} else if 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
}
} else {
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 +153,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 +167,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 +176,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 +210,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