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
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
51 if err := internal.PrerequisitePluginsReady(ienode, prerequisitePlugins...); err != nil {
52 return reconcile.ResultRequeue, err
53 }
54 p.config = cfg
55
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
63 netplanCfg, err := p.generate(ctx)
64 if err != nil {
65 return reconcile.ResultRequeue, err
66 }
67
68
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
80
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
88 tunnelSubnet, err := GetTunnelSubnet(ctx, p.config.GetClient())
89 if err != nil {
90 return nil, err
91 }
92
93
94 return GenerateTargetNetplanConfig(p.ienode, allK8sNodes, apiConfigured, p.gatewayEnabled, tunnelSubnet)
95 }
96
97
98 func (p Plugin) apply(ctx context.Context, targetNetplan *Config) error {
99
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
109 if string(currentNetplan) == string(targetNetplanBytes) {
110 return nil
111 }
112
113 changed, err := p.writeNetplan(currentNetplan, targetNetplanBytes)
114 if err != nil {
115 return err
116 }
117 if !changed {
118 return nil
119 }
120
121 if err := p.dbusApply(ctx, currentNetplan); err != nil {
122 return err
123 }
124 if p.ienode.Spec.IsGatewayNode() {
125
126 return nil
127 }
128
129 ifaceList := slices.Sorted(maps.Keys(targetNetplan.Tunnels))
130 if len(ifaceList) == 0 {
131 return nil
132 }
133
134 changed, err = configureRPFilterSetting(ctx, routing.RPFilterLoose, ifaceList)
135 if err != nil {
136 return err
137 }
138 if changed {
139 ctrl.LoggerFrom(ctx).Info("configured loose RPF", "ifaces", ifaceList)
140 }
141 return nil
142 }
143
144
145
146 func (p Plugin) writeNetplan(currentNetplan []byte, targetNetplan []byte) (changed bool, err error) {
147
148 if err := afero.WriteFile(p.config.Fs(), netplanBackupFile, currentNetplan, netplanFilePerms); err != nil {
149 return false, err
150 }
151
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
159
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
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
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