...

Source file src/edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/plugins/networking/netplan/netplan.go

Documentation: edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/plugins/networking/netplan

     1  package netplan
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"io/fs"
     7  	"maps"
     8  	"net"
     9  	"slices"
    10  
    11  	"github.com/spf13/afero"
    12  	"gopkg.in/yaml.v3"
    13  	v1 "k8s.io/api/core/v1"
    14  	ctrl "sigs.k8s.io/controller-runtime"
    15  	"sigs.k8s.io/controller-runtime/pkg/client"
    16  
    17  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile"
    18  	v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1"
    19  	"edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/config"
    20  	"edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/internal"
    21  	"edge-infra.dev/pkg/sds/ien/network/info"
    22  	"edge-infra.dev/pkg/sds/lib/networking/routing"
    23  )
    24  
    25  var (
    26  	netplanDir                    = "/host-etc/netplan/"
    27  	netplanFile                   = netplanDir + "zynstra-netplan.yaml"
    28  	netplanFilePerms  fs.FileMode = 0600
    29  	netplanBackupFile             = netplanFile + ".backup"
    30  )
    31  
    32  var (
    33  	apiConfigured = false
    34  )
    35  
    36  // ensure dhclient deprecation and iptables have run successfully before running netplan plugin
    37  var prerequisitePlugins = []string{
    38  	"dhclient",
    39  	"iptables",
    40  }
    41  
    42  type Plugin struct {
    43  	gatewayEnabled bool
    44  	ienode         *v1ien.IENode
    45  	config         config.Config
    46  }
    47  
    48  func (p Plugin) Reconcile(ctx context.Context, ienode *v1ien.IENode, cfg config.Config) (reconcile.Result, error) {
    49  	log := ctrl.LoggerFrom(ctx)
    50  	// make sure other plugins completed before we reconcile this one
    51  	if err := internal.PrerequisitePluginsReady(ienode, prerequisitePlugins...); err != nil {
    52  		return reconcile.ResultRequeue, err
    53  	}
    54  	p.config = cfg
    55  	// check if egress gateway is enabled on the cluster or not
    56  	gatewayEnabled, err := p.isGatewayEnabled(ctx)
    57  	if err != nil {
    58  		return reconcile.ResultRequeue, err
    59  	}
    60  	p.gatewayEnabled = gatewayEnabled
    61  	p.ienode = ienode
    62  	// generate target netplan config based on cluster/node config
    63  	netplanCfg, err := p.generate(ctx)
    64  	if err != nil {
    65  		return reconcile.ResultRequeue, err
    66  	}
    67  	// patch the IENode with network information
    68  	// we still need to apply netplan config if patch fails
    69  	if err := patchIENode(ctx, p.config.GetClient(), p.ienode); err != nil {
    70  		log.Error(err, "failed to patch IENode network status", "hostname", p.ienode.ObjectMeta.Name)
    71  	}
    72  	if err := p.apply(ctx, netplanCfg); err != nil {
    73  		return reconcile.ResultRequeue, err
    74  	}
    75  
    76  	return reconcile.ResultSuccess, nil
    77  }
    78  
    79  // generate generates the target netplan config for the contextual
    80  // node/cluster config
    81  func (p Plugin) generate(ctx context.Context) (*Config, error) {
    82  	allK8sNodes, err := p.GetAllK8sNodes(ctx)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	// Load in the target subnet from the config map
    88  	tunnelSubnet, err := GetTunnelSubnet(ctx, p.config.GetClient())
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	// Using the new node config, generate the target netplan file
    94  	return GenerateTargetNetplanConfig(p.ienode, allK8sNodes, apiConfigured, p.gatewayEnabled, tunnelSubnet)
    95  }
    96  
    97  // apply is responsible for applying the netplan configuration to the system.
    98  func (p Plugin) apply(ctx context.Context, targetNetplan *Config) error {
    99  	// Read the current netplan file
   100  	currentNetplan, err := afero.ReadFile(p.config.Fs(), netplanFile)
   101  	if err != nil {
   102  		return err
   103  	}
   104  	targetNetplanBytes, err := formatNetplanYaml(targetNetplan)
   105  	if err != nil {
   106  		return err
   107  	}
   108  	// Compare the current netplan file again the target netplan file
   109  	if string(currentNetplan) == string(targetNetplanBytes) {
   110  		return nil
   111  	}
   112  	// write netplan yaml
   113  	changed, err := p.writeNetplan(currentNetplan, targetNetplanBytes)
   114  	if err != nil {
   115  		return err
   116  	}
   117  	if !changed { // no netplan updates so return early
   118  		return nil
   119  	}
   120  	// apply the netplan configuration via dbus
   121  	if err := p.dbusApply(ctx, currentNetplan); err != nil {
   122  		return err
   123  	}
   124  	if p.ienode.Spec.IsGatewayNode() {
   125  		// don't need to update rp_filter settings on gateway nodes
   126  		return nil
   127  	}
   128  	// sorted list of tunnel names - should be empty if egw disabled
   129  	ifaceList := slices.Sorted(maps.Keys(targetNetplan.Tunnels))
   130  	if len(ifaceList) == 0 {
   131  		return nil // nothing to set
   132  	}
   133  	// configure reverse path forwarding setting for tunnels from non-gw node to gw node(s)
   134  	changed, err = configureRPFilterSetting(ctx, routing.RPFilterLoose, ifaceList)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	if changed { // only log if we actually did something
   139  		ctrl.LoggerFrom(ctx).Info("configured loose RPF", "ifaces", ifaceList)
   140  	}
   141  	return nil
   142  }
   143  
   144  // writeNetplan writes the target netplan configuration to the system
   145  // and backs up the current netplan configuration.
   146  func (p Plugin) writeNetplan(currentNetplan []byte, targetNetplan []byte) (changed bool, err error) {
   147  	// Backup current netplan file
   148  	if err := afero.WriteFile(p.config.Fs(), netplanBackupFile, currentNetplan, netplanFilePerms); err != nil {
   149  		return false, err
   150  	}
   151  	// Write new netplan config
   152  	if err := afero.WriteFile(p.config.Fs(), netplanFile, targetNetplan, netplanFilePerms); err != nil {
   153  		return false, err
   154  	}
   155  	return true, nil
   156  }
   157  
   158  // dbusApply applies the netplan configuration using the dbus api.
   159  // Reverts the changes if the netplan apply failed.
   160  func (p Plugin) dbusApply(ctx context.Context, currentNetplan []byte) error {
   161  	log := ctrl.LoggerFrom(ctx)
   162  	if err := p.config.NetplanAPI().Connect(); err != nil {
   163  		return err
   164  	}
   165  	defer p.config.NetplanAPI().Close()
   166  	// Attempting to apply changes using dbus api
   167  	output, netplanErr := p.config.NetplanAPI().Apply()
   168  	if netplanErr == nil {
   169  		log.Info("Netplan config applied.")
   170  		return nil
   171  	}
   172  	log.Error(netplanErr, "failed to apply netplan config", "reason", output)
   173  	log.Info("reverting netplan config")
   174  	// Revert changes if netplan try failed to apply new changes
   175  	if err := afero.WriteFile(p.config.Fs(), netplanFile, currentNetplan, netplanFilePerms); err != nil {
   176  		return err
   177  	}
   178  	return netplanErr
   179  }
   180  
   181  func formatNetplanYaml(config *Config) ([]byte, error) {
   182  	var buffer bytes.Buffer
   183  	yamlEncoder := yaml.NewEncoder(&buffer)
   184  	yamlEncoder.SetIndent(2)
   185  	err := yamlEncoder.Encode(&config)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	return buffer.Bytes(), nil
   190  }
   191  
   192  func (p Plugin) isGatewayEnabled(ctx context.Context) (bool, error) {
   193  	configMap := v1.ConfigMap{}
   194  	if err := p.config.GetClient().Get(ctx, client.ObjectKey{Name: "topology-info", Namespace: "kube-public"}, &configMap); err != nil {
   195  		return false, err
   196  	}
   197  	if egressGateway, ok := configMap.Data["egress_gateway_enabled"]; !ok || egressGateway == "false" {
   198  		return false, nil
   199  	}
   200  	return true, nil
   201  }
   202  
   203  func (p Plugin) GetAllK8sNodes(ctx context.Context) ([]v1.Node, error) {
   204  	nodeList := v1.NodeList{}
   205  
   206  	if err := p.config.GetClient().List(ctx, &nodeList, &client.ListOptions{}); err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	return nodeList.Items, nil
   211  }
   212  
   213  func GetTunnelSubnet(ctx context.Context, client client.Client) (*net.IPNet, error) {
   214  	netInfo, err := info.New().FromClient(ctx, client)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	_, parsedSubnet, err := net.ParseCIDR(netInfo.EgressTunnelSubnet)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	return parsedSubnet, nil
   225  }
   226  

View as plain text