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 3 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"`
}
176 changes: 146 additions & 30 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 Down Expand Up @@ -38,7 +40,23 @@ var Plugin = plugins.Plugin{
// map MAC address to inventory name
var inventoryMap map[string]string

// args[0] = path to configuration file
// default inventory name prefix
const defaultNamePrefix = "compute-"

type OnBoardingStrategy int

const (
OnboardingFromStaticList = 1
damyan marked this conversation as resolved.
Show resolved Hide resolved
OnboardingFromMACPrefixFilter = 2
)

// flag for onboarding strategy:
// OnboardingFromStaticList: the name comes from a static inventory list and shall be taken "as is"
//
// OnboardingFromMACPrefixFilter: the name is only a prefix (default or custom), k8s will autogenerate it
var strategy OnBoardingStrategy

// 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 @@ -52,6 +70,9 @@ func setup6(args ...string) (handler.Handler6, error) {
if err != nil {
return nil, err
}
if inventoryMap == nil {
return nil, nil
}

return handler6, nil
}
Expand All @@ -62,22 +83,40 @@ func loadConfig(args ...string) (map[string]string, error) {
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
// static inventory list has precedence, always
if len(config.Inventories) > 0 {
damyan marked this conversation as resolved.
Show resolved Hide resolved
strategy = OnboardingFromStaticList
log.Debug("Using static list onboarding")
for _, i := range config.Inventories {
if i.MacAddress != "" && i.Name != "" {
inventories[strings.ToLower(i.MacAddress)] = i.Name
}
}
} else if len(config.Filter.MacPrefix) > 0 {
strategy = OnboardingFromMACPrefixFilter
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 {
inventories[strings.ToLower(i)] = namePrefix
}
} else {
log.Infof("No inventories loaded")
return nil, nil
}

log.Infof("Loaded metal config with %d inventories", len(inventories))
Expand All @@ -90,6 +129,9 @@ func setup4(args ...string) (handler.Handler4, error) {
if err != nil {
return nil, err
}
if inventoryMap == nil {
return nil, nil
}

return handler4, nil
}
Expand All @@ -109,7 +151,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 +165,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 +174,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 +208,104 @@ 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 strategy == OnboardingFromStaticList {
// 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)
}
} else if strategy == OnboardingFromMACPrefixFilter {
// 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())
epPatch := client.MergeFrom(existingEndpoint.DeepCopy())
existingEndpoint.Spec.IP = metalv1alpha1.MustParseIP(ip.String())
if err := cl.Patch(ctx, existingEndpoint, epPatch); 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)
}
}
} else {
return fmt.Errorf("unknown OnboardingStrategy %d", strategy)
}

return nil
}

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

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

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

func GetInventoryEntryMatchingMACAddress(mac net.HardwareAddr) string {
if strategy == OnboardingFromStaticList {
inventoryName, ok := inventoryMap[strings.ToLower(mac.String())]
if !ok {
log.Debugf("Unknown inventory MAC address: %s", mac.String())
} else {
return inventoryName
}
} else if strategy == OnboardingFromMACPrefixFilter {
for i := range inventoryMap {
if strings.HasPrefix(strings.ToLower(mac.String()), strings.ToLower(i)) {
return inventoryMap[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())
} else {
log.Debugf("Unknown Onboarding strategy %d", strategy)
}

return ""
}

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

Expand Down
Loading
Loading