From 799a79107c71a0db19252042dd5cbe69afa0d19e Mon Sep 17 00:00:00 2001 From: Kris Raney Date: Wed, 12 Aug 2026 00:45:37 +0000 Subject: [PATCH 1/7] Fix pod-to-pod and external access to Juniper mgmt interface Have the init container write default route to starting config Juniper and Cisco both ignore the route on eth0 and start from scratch. This uses the initContainer to pick up the route and patch it into the starting config for those types. --- cloudbuild/vendors_test.sh | 16 +-- topo/node/cisco/cisco.go | 145 +++++++++++++++++++++++----- topo/node/cisco/cisco_test.go | 37 +++++++ topo/node/juniper/juniper.go | 149 ++++++++++++++++++++-------- topo/node/juniper/juniper_test.go | 155 ++++++++++++++++++++---------- topo/node/node.go | 4 +- 6 files changed, 380 insertions(+), 126 deletions(-) mode change 100644 => 100755 cloudbuild/vendors_test.sh diff --git a/cloudbuild/vendors_test.sh b/cloudbuild/vendors_test.sh old mode 100644 new mode 100755 index ba792450b..35a06b013 --- a/cloudbuild/vendors_test.sh +++ b/cloudbuild/vendors_test.sh @@ -19,7 +19,7 @@ export PATH=${PATH}:/usr/local/go/bin gopath=$(go env GOPATH) export PATH=${PATH}:$gopath/bin -# Replace exisiting kne repo with new version +# Replace existing kne repo with new version rm -r "$HOME/kne" cp -r /tmp/workspace "$HOME/kne" @@ -37,11 +37,11 @@ popd # Run an ondatra test pushd "$HOME/kne/cloudbuild" -go test -v vendors/vendors_test.go \ - -testbed testbed.textproto \ - -topology topology.textproto \ - -vendor_creds ARISTA/admin/admin \ - -vendor_creds JUNIPER/root/Google123 \ - -vendor_creds CISCO/cisco/cisco123 \ - -vendor_creds NOKIA/admin/NokiaSrl1! +go test -v -timeout 30m vendors/vendors_test.go \ + -testbed testbed.textproto \ + -topology topology.textproto \ + -vendor_creds ARISTA/admin/admin \ + -vendor_creds JUNIPER/root/Google123 \ + -vendor_creds CISCO/cisco/cisco123 \ + -vendor_creds NOKIA/admin/NokiaSrl1! popd diff --git a/topo/node/cisco/cisco.go b/topo/node/cisco/cisco.go index 5fb560db8..aee484aa9 100644 --- a/topo/node/cisco/cisco.go +++ b/topo/node/cisco/cisco.go @@ -189,15 +189,6 @@ func (n *Node) Create(ctx context.Context) error { }, }, Spec: corev1.PodSpec{ - InitContainers: []corev1.Container{{ - Name: fmt.Sprintf("init-%s", n.Name()), - Image: initContainerImage, - Args: []string{ - fmt.Sprintf("%d", len(pb.GetInterfaces())+1), - fmt.Sprintf("%d", pb.GetConfig().Sleep), - }, - ImagePullPolicy: "IfNotPresent", - }}, Containers: []corev1.Container{{ Name: n.Name(), Image: pb.Config.Image, @@ -247,22 +238,107 @@ func (n *Node) Create(ctx context.Context) error { for label, v := range n.GetProto().GetLabels() { pod.ObjectMeta.Labels[label] = v } - if pb.Config.ConfigData != nil { - vol, err := n.CreateConfig(ctx) - if err != nil { - return err + + if pb.Model == ModelXRD { + configFile := pb.Config.ConfigFile + if configFile == "" { + configFile = "startup.cfg" } - pod.Spec.Volumes = append(pod.Spec.Volumes, *vol) + configDstPath := "/config-dst" + configSrcPath := "/config-src" + initScript := fmt.Sprintf(` +/entrypoint.sh "$1" "$2" +mkdir -p %[1]s +if [ -f %[2]s/%[3]s ]; then + cp %[2]s/%[3]s %[1]s/%[3]s +else + touch %[1]s/%[3]s +fi +GW4=$(ip -4 route show default 2>/dev/null | awk '{print $3}' | head -n1) +if [ -n "$GW4" ]; then + printf '\nrouter static\n address-family ipv4 unicast\n 0.0.0.0/0 %%s\n !\n!\n' "$GW4" >> %[1]s/%[3]s +fi +GW6=$(ip -6 route show default 2>/dev/null | awk '{print $3}' | head -n1) +if [ -n "$GW6" ]; then + printf '\nrouter static\n address-family ipv6 unicast\n ::/0 %%s\n !\n!\n' "$GW6" >> %[1]s/%[3]s +fi +`, configDstPath, configSrcPath, configFile) + + var initVolumeMounts []corev1.VolumeMount + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: node.ConfigVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + initVolumeMounts = append(initVolumeMounts, corev1.VolumeMount{ + Name: node.ConfigVolumeName, + MountPath: configDstPath, + }) vm := corev1.VolumeMount{ Name: node.ConfigVolumeName, - MountPath: pb.Config.ConfigPath + "/" + pb.Config.ConfigFile, + MountPath: pb.Config.ConfigPath + "/" + configFile, + SubPath: configFile, ReadOnly: true, } - if vol.VolumeSource.ConfigMap != nil { - vm.SubPath = pb.Config.ConfigFile + for i := range pod.Spec.Containers { + pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, vm) } - for i, c := range pod.Spec.Containers { - pod.Spec.Containers[i].VolumeMounts = append(c.VolumeMounts, vm) + + if pb.Config.ConfigData != nil { + vol, err := n.CreateConfig(ctx) + if err != nil { + return err + } + vol.Name = "startup-config-src-volume" + pod.Spec.Volumes = append(pod.Spec.Volumes, *vol) + initVolumeMounts = append(initVolumeMounts, corev1.VolumeMount{ + Name: "startup-config-src-volume", + MountPath: configSrcPath, + ReadOnly: true, + }) + } + + pod.Spec.InitContainers = []corev1.Container{{ + Name: fmt.Sprintf("init-%s", n.Name()), + Image: initContainerImage, + Command: []string{"/bin/sh", "-c"}, + Args: []string{ + initScript, + "init", + fmt.Sprintf("%d", len(pb.GetInterfaces())+1), + fmt.Sprintf("%d", pb.GetConfig().Sleep), + }, + ImagePullPolicy: "IfNotPresent", + VolumeMounts: initVolumeMounts, + }} + } else { + pod.Spec.InitContainers = []corev1.Container{{ + Name: fmt.Sprintf("init-%s", n.Name()), + Image: initContainerImage, + Args: []string{ + fmt.Sprintf("%d", len(pb.GetInterfaces())+1), + fmt.Sprintf("%d", pb.GetConfig().Sleep), + }, + ImagePullPolicy: "IfNotPresent", + }} + if pb.Config.ConfigData != nil { + vol, err := n.CreateConfig(ctx) + if err != nil { + return err + } + pod.Spec.Volumes = append(pod.Spec.Volumes, *vol) + vm := corev1.VolumeMount{ + Name: node.ConfigVolumeName, + MountPath: pb.Config.ConfigPath + "/" + pb.Config.ConfigFile, + ReadOnly: true, + } + if vol.VolumeSource.ConfigMap != nil { + vm.SubPath = pb.Config.ConfigFile + } + for i, c := range pod.Spec.Containers { + pod.Spec.Containers[i].VolumeMounts = append(c.VolumeMounts, vm) + } } } sPod, err := n.KubeClient.CoreV1().Pods(n.Namespace).Create(ctx, pod, metav1.CreateOptions{}) @@ -618,16 +694,22 @@ func (n *Node) SpawnCLIConn() error { } var err error n.cliConn, err = n.GetCLIConn(scrapliPlatformName, opts) + if err != nil { + return err + } // TODO: add the following pattern in the scrapli/scrapligo/blob/main/assets/platforms/cisco_iosxr.yaml n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "ERROR") n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "% Failed") n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "No such file or directory") + n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "locked") + n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "% Configuration database") + n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "% Cannot commit") if n.Proto.Model != ModelXRD { n.cliConn.OnClose = endTelnet } - return err + return nil } // SpawnCLIConnConf spawns a connection towards a IOSXR configuration CLI for XRd using `kubectl exec` terminal @@ -651,8 +733,17 @@ func (n *Node) SpawnCLIConnConf() error { opts = n.PatchCLIConnOpen("kubectl", []string{"bash", "/pkg/bin/xr_cli", "config"}, opts) var err error n.cliConn, err = n.GetCLIConn(scrapliPlatformName, opts) + if err != nil { + return err + } + n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "ERROR") + n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "% Failed") + n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "No such file or directory") + n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "locked") + n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "% Configuration database") + n.cliConn.FailedWhenContains = append(n.cliConn.FailedWhenContains, "% Cannot commit") - return err + return nil } func endTelnet(d *scraplinetwork.Driver) error { @@ -684,7 +775,7 @@ func (n *Node) ResetCfg(ctx context.Context) error { var cmd string if n.Proto.Model == ModelXRD { - // Copy the snooped management interface config from a know location and the startup config from + // Copy the snooped management interface config from a known location and the startup config from // the mounted location so it can be applied. This is required to preserve the snooped management // IP address and since the "copy" xr_cli command can only access files on disk 0/1. // Send an additional return command to make sure any error messages are read. @@ -756,11 +847,15 @@ func (n *Node) ConfigPush(ctx context.Context, r io.Reader) error { if err != nil { return err } - if resp.Failed == nil { - log.Infof("%s - finished config push", n.Impl.Proto.Name) + if resp.Failed != nil { + return resp.Failed + } + if strings.Contains(resp.Result, "% ") || strings.Contains(resp.Result, "error:") { + return fmt.Errorf("config push failed: %s", resp.Result) } - return resp.Failed + log.Infof("%s - finished config push", n.Name()) + return nil } func (n *Node) GenerateSelfSigned(context.Context) error { diff --git a/topo/node/cisco/cisco_test.go b/topo/node/cisco/cisco_test.go index bcea70f65..a8938202e 100644 --- a/topo/node/cisco/cisco_test.go +++ b/topo/node/cisco/cisco_test.go @@ -1137,6 +1137,43 @@ func TestGenerateSelfSigned(t *testing.T) { } } +func TestCreate(t *testing.T) { + ki := fake.NewSimpleClientset() + n := &Node{ + Impl: &node.Impl{ + Namespace: "test", + KubeClient: ki, + Proto: &tpb.Node{ + Name: "xrd", + Model: ModelXRD, + Config: &tpb.Config{ + ConfigFile: "startup.cfg", + ConfigPath: "/", + ConfigData: &tpb.Config_Data{ + Data: []byte("hostname xrd"), + }, + }, + Interfaces: map[string]*tpb.Interface{ + "eth1": {Name: "GigabitEthernet0/0/0/0"}, + }, + }, + }, + } + if err := n.Create(context.Background()); err != nil { + t.Fatalf("Create() unexpected error = %v", err) + } + pod, err := ki.CoreV1().Pods("test").Get(context.Background(), "xrd", metav1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get created pod: %v", err) + } + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + if len(pod.Spec.InitContainers[0].VolumeMounts) != 2 { + t.Fatalf("expected 2 volume mounts in init container, got %d", len(pod.Spec.InitContainers[0].VolumeMounts)) + } +} + func TestDefaultNodeConstraints(t *testing.T) { tests := []struct { name string diff --git a/topo/node/juniper/juniper.go b/topo/node/juniper/juniper.go index 2c1ef8e0c..29aa07ad6 100644 --- a/topo/node/juniper/juniper.go +++ b/topo/node/juniper/juniper.go @@ -36,11 +36,11 @@ var ( // Wait for PKI cert infra certGenTimeout = 15 * time.Minute // Time between polls - certGenRetrySleep = 30 * time.Second + certGenRetrySleep = 5 * time.Second // Wait for config mode configModeTimeout = 15 * time.Minute // Time between polls - config mode - configModeRetrySleep = 30 * time.Second + configModeRetrySleep = 5 * time.Second // Default gRPC port defaultGrpcPort = uint32(9339) @@ -297,6 +297,9 @@ func (n *Node) waitCertInfraReadyAndPushCert() error { // GenerateSelfSigned generates a self-signed TLS certificate using Junos PKI func (n *Node) GenerateSelfSigned(ctx context.Context) error { + if n.KubeClient == nil { + return errors.New("kubeclient is nil") + } selfSigned := n.Proto.GetConfig().GetCert().GetSelfSigned() if selfSigned == nil { log.Infof("%s - no cert config", n.Name()) @@ -308,24 +311,38 @@ func (n *Node) GenerateSelfSigned(ctx context.Context) error { } log.Infof("%s - generating self signed certs", n.Name()) log.Infof("%s - waiting for pod to be running", n.Name()) - w, err := n.KubeClient.CoreV1().Pods(n.Namespace).Watch(ctx, metav1.ListOptions{ - FieldSelector: fields.SelectorFromSet( - fields.Set{metav1.ObjectNameField: n.Name()}, - ).String(), - }) - if err != nil { - return err - } - for e := range w.ResultChan() { - p, ok := e.Object.(*corev1.Pod) - if !ok { - continue + pod, err := n.KubeClient.CoreV1().Pods(n.Namespace).Get(ctx, n.Name(), metav1.GetOptions{}) + if err == nil && pod.Status.Phase == corev1.PodRunning { + log.Infof("%s - pod already running.", n.Name()) + } else { + w, err := n.KubeClient.CoreV1().Pods(n.Namespace).Watch(ctx, metav1.ListOptions{ + FieldSelector: fields.SelectorFromSet( + fields.Set{metav1.ObjectNameField: n.Name()}, + ).String(), + }) + if err != nil { + return err } - if p.Status.Phase == corev1.PodRunning { - break + defer w.Stop() + var running bool + for e := range w.ResultChan() { + p, ok := e.Object.(*corev1.Pod) + if !ok { + continue + } + if p.Status.Phase == corev1.PodRunning { + running = true + break + } + } + if !running { + if ctx.Err() != nil { + return ctx.Err() + } + return fmt.Errorf("%s - watch closed before pod reached running phase", n.Name()) } + log.Infof("%s - pod running.", n.Name()) } - log.Infof("%s - pod running.", n.Name()) if err := n.SpawnCLIConn(); err != nil { return err @@ -448,7 +465,8 @@ func (n *Node) ResetCfg(ctx context.Context) error { // Reset applies factory config which doesn't contain gRPC config // send gRPC config - multiresp, err = n.cliConn.SendConfigs(n.GRPCConfig()) + grpcConfigs := n.GRPCConfig() + multiresp, err = n.cliConn.SendConfigs(grpcConfigs) if err != nil { return err } @@ -490,15 +508,6 @@ func (n *Node) Create(ctx context.Context) error { }, }, Spec: corev1.PodSpec{ - InitContainers: []corev1.Container{{ - Name: fmt.Sprintf("init-%s", n.Name()), - Image: initContainerImage, - Args: []string{ - fmt.Sprintf("%d", len(pb.GetInterfaces())+1), - fmt.Sprintf("%d", pb.GetConfig().Sleep), - }, - ImagePullPolicy: "IfNotPresent", - }}, Containers: []corev1.Container{{ Name: n.Name(), Image: pb.Config.Image, @@ -600,24 +609,88 @@ func (n *Node) Create(ctx context.Context) error { for label, v := range n.GetProto().GetLabels() { pod.ObjectMeta.Labels[label] = v } + + configFile := pb.Config.ConfigFile + if configFile == "" { + configFile = "juniper.conf" + } + configDstPath := "/config-dst" + configSrcPath := "/config-src" + initScript := fmt.Sprintf(` +/entrypoint.sh "$1" "$2" +mkdir -p %[1]s +if [ -f %[2]s/%[3]s ]; then + cp %[2]s/%[3]s %[1]s/%[3]s +else + touch %[1]s/%[3]s +fi +IP4=$(ip -4 addr show dev eth0 2>/dev/null | awk '/inet /{print $2}' | head -n1) +GW4=$(ip -4 route show default 2>/dev/null | awk '{print $3}' | head -n1) +if [ -n "$IP4" ]; then + printf '\nset interfaces re0:mgmt-0 unit 0 family inet address %%s\n' "$IP4" >> %[1]s/%[3]s +fi +if [ -n "$GW4" ]; then + printf 'set routing-options static route 0.0.0.0/0 next-hop %%s\n' "$GW4" >> %[1]s/%[3]s +fi +IP6=$(ip -6 addr show dev eth0 2>/dev/null | awk '/inet6 /{print $2}' | grep -v '^fe80' | head -n1) +GW6=$(ip -6 route show default 2>/dev/null | awk '{print $3}' | head -n1) +if [ -n "$IP6" ]; then + printf '\nset interfaces re0:mgmt-0 unit 0 family inet6 address %%s\n' "$IP6" >> %[1]s/%[3]s +fi +if [ -n "$GW6" ]; then + printf 'set routing-options rib inet6.0 static route ::/0 next-hop %%s\n' "$GW6" >> %[1]s/%[3]s +fi +`, configDstPath, configSrcPath, configFile) + + var initVolumeMounts []corev1.VolumeMount + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: node.ConfigVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + initVolumeMounts = append(initVolumeMounts, corev1.VolumeMount{ + Name: node.ConfigVolumeName, + MountPath: configDstPath, + }) + vm := corev1.VolumeMount{ + Name: node.ConfigVolumeName, + MountPath: pb.Config.ConfigPath + "/" + configFile, + SubPath: configFile, + ReadOnly: true, + } + for i := range pod.Spec.Containers { + pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, vm) + } + if pb.Config.ConfigData != nil { vol, err := n.CreateConfig(ctx) if err != nil { return err } + vol.Name = "startup-config-src-volume" pod.Spec.Volumes = append(pod.Spec.Volumes, *vol) - vm := corev1.VolumeMount{ - Name: node.ConfigVolumeName, - MountPath: pb.Config.ConfigPath + "/" + pb.Config.ConfigFile, + initVolumeMounts = append(initVolumeMounts, corev1.VolumeMount{ + Name: "startup-config-src-volume", + MountPath: configSrcPath, ReadOnly: true, - } - if vol.VolumeSource.ConfigMap != nil { - vm.SubPath = pb.Config.ConfigFile - } - for i, c := range pod.Spec.Containers { - pod.Spec.Containers[i].VolumeMounts = append(c.VolumeMounts, vm) - } - } + }) + } + + pod.Spec.InitContainers = []corev1.Container{{ + Name: fmt.Sprintf("init-%s", n.Name()), + Image: initContainerImage, + Command: []string{"/bin/sh", "-c"}, + Args: []string{ + initScript, + "init", + fmt.Sprintf("%d", len(pb.GetInterfaces())+1), + fmt.Sprintf("%d", pb.GetConfig().Sleep), + }, + ImagePullPolicy: "IfNotPresent", + VolumeMounts: initVolumeMounts, + }} + sPod, err := n.KubeClient.CoreV1().Pods(n.Namespace).Create(ctx, pod, metav1.CreateOptions{}) if err != nil { return fmt.Errorf("failed to create pod for %q: %w", pb.Name, err) diff --git a/topo/node/juniper/juniper_test.go b/topo/node/juniper/juniper_test.go index 9bda8259c..3d1e77510 100644 --- a/topo/node/juniper/juniper_test.go +++ b/topo/node/juniper/juniper_test.go @@ -30,10 +30,6 @@ import ( ktest "k8s.io/client-go/testing" ) -type fakeWatch struct { - e []watch.Event -} - // scrapliDebug checks if SCRAPLI_DEBUG env var is set. // used in testing to enable debug log of scrapligo. func scrapliDebug() bool { @@ -42,20 +38,6 @@ func scrapliDebug() bool { return set } -func (f *fakeWatch) Stop() {} - -func (f *fakeWatch) ResultChan() <-chan watch.Event { - eCh := make(chan watch.Event) - go func() { - for len(f.e) != 0 { - e := f.e[0] - f.e = f.e[1:] - eCh <- e - } - }() - return eCh -} - // removeCommentsFromConfig removes comment lines from a JunOS config file // and returns the remaining config in an io.Reader. // Using scrapli_cfg_testing results in an EOF error when config includes comments. @@ -91,22 +73,15 @@ func TestGenerateSelfSigned(t *testing.T) { }) reaction := func(action ktest.Action) (handled bool, ret watch.Interface, err error) { - f := &fakeWatch{ - e: []watch.Event{ - { - // Test that watcher properly handles events with the wrong type. - Object: &corev1.ConfigMap{}, - }, - { - Object: &corev1.Pod{ - Status: corev1.PodStatus{ - Phase: corev1.PodRunning, - }, - }, - }, + fw := watch.NewFakeWithChanSize(2, false) + // Test that watcher properly handles events with the wrong type. + fw.Add(&corev1.ConfigMap{}) + fw.Add(&corev1.Pod{ + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, }, - } - return true, f, nil + }) + return true, fw, nil } ki.PrependWatchReactor("*", reaction) @@ -188,6 +163,34 @@ func TestGenerateSelfSigned(t *testing.T) { ni: ni, testFile: "testdata/generate_certificate_config_mode_failure", }, + { + // nil kubeclient + desc: "nil kubeclient", + wantErr: true, + ni: &node.Impl{ + Namespace: "test", + Proto: ni.Proto, + }, + }, + { + // pod already running + desc: "pod already running", + wantErr: false, + testFile: "testdata/generate_certificate_success", + ni: &node.Impl{ + KubeClient: fake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "test", + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + }), + Namespace: "test", + Proto: ni.Proto, + }, + }, { // invalid cert name contains invalid characters desc: "invalid cert name characters", @@ -327,16 +330,13 @@ func TestConfigPush(t *testing.T) { }) reaction := func(action ktest.Action) (handled bool, ret watch.Interface, err error) { - f := &fakeWatch{ - e: []watch.Event{{ - Object: &corev1.Pod{ - Status: corev1.PodStatus{ - Phase: corev1.PodRunning, - }, - }, - }}, - } - return true, f, nil + fw := watch.NewFakeWithChanSize(1, false) + fw.Add(&corev1.Pod{ + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + }) + return true, fw, nil } ki.PrependWatchReactor("*", reaction) @@ -421,19 +421,28 @@ func TestResetCfg(t *testing.T) { }) reaction := func(action ktest.Action) (handled bool, ret watch.Interface, err error) { - f := &fakeWatch{ - e: []watch.Event{{ - Object: &corev1.Pod{ - Status: corev1.PodStatus{ - Phase: corev1.PodRunning, - }, - }, - }}, - } - return true, f, nil + fw := watch.NewFakeWithChanSize(1, false) + fw.Add(&corev1.Pod{ + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + }) + return true, fw, nil } ki.PrependWatchReactor("*", reaction) + origConfigModeRetrySleep := configModeRetrySleep + defer func() { + configModeRetrySleep = origConfigModeRetrySleep + }() + configModeRetrySleep = time.Millisecond + + origConfigModeTimeout := configModeTimeout + defer func() { + configModeTimeout = origConfigModeTimeout + }() + configModeTimeout = 100 * time.Millisecond + ni := &node.Impl{ KubeClient: ki, Namespace: "test", @@ -915,3 +924,43 @@ func TestValidCertNameRegexp(t *testing.T) { }) } } + +func TestCreate(t *testing.T) { + ki := fake.NewSimpleClientset() + ni := &node.Impl{ + Namespace: "test", + KubeClient: ki, + Proto: &tpb.Node{ + Name: "ncptx", + Model: "ncptx", + Vendor: tpb.Vendor_JUNIPER, + Config: &tpb.Config{ + ConfigFile: "juniper.conf", + ConfigPath: "/home/evo/configdisk", + ConfigData: &tpb.Config_Data{ + Data: []byte("set system host-name ncptx"), + }, + }, + Interfaces: map[string]*tpb.Interface{ + "eth1": {Name: "et-0/0/0:0"}, + }, + }, + } + n, err := New(ni) + if err != nil { + t.Fatalf("New() unexpected error = %v", err) + } + if err := n.Create(context.Background()); err != nil { + t.Fatalf("Create() unexpected error = %v", err) + } + pod, err := ki.CoreV1().Pods("test").Get(context.Background(), "ncptx", metav1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get created pod: %v", err) + } + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + if len(pod.Spec.InitContainers[0].VolumeMounts) != 2 { + t.Fatalf("expected 2 volume mounts in init container, got %d", len(pod.Spec.InitContainers[0].VolumeMounts)) + } +} diff --git a/topo/node/node.go b/topo/node/node.go index 114b3ed71..df40f565e 100644 --- a/topo/node/node.go +++ b/topo/node/node.go @@ -12,9 +12,9 @@ import ( "sync" "time" - topologyv1 "github.com/openconfig/kne/third_party/meshnet/api/types/v1beta1" "github.com/openconfig/gnmi/errlist" tpb "github.com/openconfig/kne/proto/topo" + topologyv1 "github.com/openconfig/kne/third_party/meshnet/api/types/v1beta1" scraplinetwork "github.com/scrapli/scrapligo/driver/network" scrapliopts "github.com/scrapli/scrapligo/driver/options" scraplilogging "github.com/scrapli/scrapligo/logging" @@ -627,7 +627,7 @@ func (n *Impl) Exec(ctx context.Context, cmd []string, stdin io.Reader, stdout i return err } log.Infof("Execing %s on %s", cmd, n.Name()) - return exec.Stream(remotecommand.StreamOptions{ + return exec.StreamWithContext(ctx, remotecommand.StreamOptions{ Stdin: stdin, Stdout: stdout, Stderr: stderr, From b55343884a69acfcd1d47e684fbc8bd69ce0a38b Mon Sep 17 00:00:00 2001 From: Kris Raney Date: Sat, 15 Aug 2026 00:00:21 +0000 Subject: [PATCH 2/7] Add test coverage --- topo/node/cisco/cisco.go | 8 +- topo/node/cisco/cisco_test.go | 208 ++++++++++++++++++++++++++---- topo/node/juniper/juniper.go | 8 +- topo/node/juniper/juniper_test.go | 202 +++++++++++++++++++++++++---- topo/node/node.go | 9 ++ 5 files changed, 372 insertions(+), 63 deletions(-) diff --git a/topo/node/cisco/cisco.go b/topo/node/cisco/cisco.go index aee484aa9..48a1ea5da 100644 --- a/topo/node/cisco/cisco.go +++ b/topo/node/cisco/cisco.go @@ -285,11 +285,9 @@ fi pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, vm) } - if pb.Config.ConfigData != nil { - vol, err := n.CreateConfig(ctx) - if err != nil { - return err - } + if vol, err := n.CreateConfig(ctx); err != nil { + return err + } else if vol != nil { vol.Name = "startup-config-src-volume" pod.Spec.Volumes = append(pod.Spec.Volumes, *vol) initVolumeMounts = append(initVolumeMounts, corev1.VolumeMount{ diff --git a/topo/node/cisco/cisco_test.go b/topo/node/cisco/cisco_test.go index a8938202e..609011aba 100644 --- a/topo/node/cisco/cisco_test.go +++ b/topo/node/cisco/cisco_test.go @@ -15,8 +15,12 @@ package cisco import ( "context" + "fmt" "os" + "os/exec" + "path/filepath" "regexp" + "strings" "testing" "time" @@ -1138,39 +1142,191 @@ func TestGenerateSelfSigned(t *testing.T) { } func TestCreate(t *testing.T) { - ki := fake.NewSimpleClientset() - n := &Node{ - Impl: &node.Impl{ - Namespace: "test", - KubeClient: ki, - Proto: &tpb.Node{ - Name: "xrd", - Model: ModelXRD, - Config: &tpb.Config{ - ConfigFile: "startup.cfg", - ConfigPath: "/", - ConfigData: &tpb.Config_Data{ - Data: []byte("hostname xrd"), + tests := []struct { + desc string + model string + configData []byte + wantInitCommand []string + wantInitArgsLen int + wantInitMountsLen int + wantMainMountsLen int + wantInitScriptSub []string + wantErr bool + }{ + { + desc: "XRD with config data", + model: ModelXRD, + configData: []byte("hostname xrd"), + wantInitCommand: []string{"/bin/sh", "-c"}, + wantInitArgsLen: 4, // script, "init", num_intfs, sleep + wantInitMountsLen: 2, // /config-dst and /config-src + wantMainMountsLen: 2, // /run and /startup.cfg + wantInitScriptSub: []string{"/entrypoint.sh", "router static", "address-family ipv4 unicast", "address-family ipv6 unicast", "/config-dst"}, + }, + { + desc: "XRD without config data", + model: ModelXRD, + configData: nil, + wantInitCommand: []string{"/bin/sh", "-c"}, + wantInitArgsLen: 4, + wantInitMountsLen: 1, // only /config-dst + wantMainMountsLen: 2, // /run and /startup.cfg + wantInitScriptSub: []string{"/entrypoint.sh", "router static", "address-family ipv4 unicast"}, + }, + { + desc: "8201 non-XRD with config data", + model: "8201", + configData: []byte("hostname 8201"), + wantInitCommand: nil, // default entrypoint + wantInitArgsLen: 2, // num_intfs, sleep + wantInitMountsLen: 0, + wantMainMountsLen: 2, // /run and ConfigMap volume + }, + { + desc: "8201 non-XRD without config data", + model: "8201", + configData: nil, + wantInitCommand: nil, + wantInitArgsLen: 2, + wantInitMountsLen: 0, + wantMainMountsLen: 1, // /run only + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ki := fake.NewSimpleClientset() + cfg := &tpb.Config{ + ConfigFile: "startup.cfg", + ConfigPath: "/", + } + if tt.configData != nil { + cfg.ConfigData = &tpb.Config_Data{Data: tt.configData} + } + n := &Node{ + Impl: &node.Impl{ + Namespace: "test", + KubeClient: ki, + Proto: &tpb.Node{ + Name: "node1", + Model: tt.model, + Config: cfg, + Interfaces: map[string]*tpb.Interface{ + "eth1": {Name: "GigabitEthernet0/0/0/0"}, + }, }, }, - Interfaces: map[string]*tpb.Interface{ - "eth1": {Name: "GigabitEthernet0/0/0/0"}, - }, - }, - }, + } + if err := n.Create(context.Background()); (err != nil) != tt.wantErr { + t.Fatalf("Create() unexpected error = %v, wantErr = %v", err, tt.wantErr) + } + pod, err := ki.CoreV1().Pods("test").Get(context.Background(), "node1", metav1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get created pod: %v", err) + } + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + initC := pod.Spec.InitContainers[0] + if diff := cmp.Diff(tt.wantInitCommand, initC.Command); diff != "" { + t.Errorf("init container command diff (-want +got):\n%s", diff) + } + if len(initC.Args) != tt.wantInitArgsLen { + t.Errorf("init container args len = %d, want %d", len(initC.Args), tt.wantInitArgsLen) + } + if len(initC.VolumeMounts) != tt.wantInitMountsLen { + t.Errorf("init container volume mounts len = %d, want %d", len(initC.VolumeMounts), tt.wantInitMountsLen) + } + if len(pod.Spec.Containers[0].VolumeMounts) != tt.wantMainMountsLen { + t.Errorf("main container volume mounts len = %d, want %d", len(pod.Spec.Containers[0].VolumeMounts), tt.wantMainMountsLen) + } + for _, sub := range tt.wantInitScriptSub { + if !strings.Contains(initC.Args[0], sub) { + t.Errorf("init container script missing expected substring %q", sub) + } + } + }) } - if err := n.Create(context.Background()); err != nil { - t.Fatalf("Create() unexpected error = %v", err) +} + +func TestXRDInitScriptExecution(t *testing.T) { + tmpDir := t.TempDir() + srcDir := filepath.Join(tmpDir, "config-src") + dstDir := filepath.Join(tmpDir, "config-dst") + binDir := filepath.Join(tmpDir, "bin") + if err := os.MkdirAll(srcDir, 0755); err != nil { + t.Fatal(err) } - pod, err := ki.CoreV1().Pods("test").Get(context.Background(), "xrd", metav1.GetOptions{}) + if err := os.MkdirAll(binDir, 0755); err != nil { + t.Fatal(err) + } + + configFile := "startup.cfg" + srcFile := filepath.Join(srcDir, configFile) + if err := os.WriteFile(srcFile, []byte("hostname xrd\n"), 0644); err != nil { + t.Fatal(err) + } + + // Create fake entrypoint.sh in binDir + entrypointPath := filepath.Join(binDir, "entrypoint.sh") + if err := os.WriteFile(entrypointPath, []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil { + t.Fatal(err) + } + + // Create fake ip command in binDir + ipPath := filepath.Join(binDir, "ip") + ipScript := `#!/bin/sh +if [ "$1" = "-4" ] && [ "$2" = "route" ]; then + echo "default via 10.244.0.1 dev eth0" +elif [ "$1" = "-6" ] && [ "$2" = "route" ]; then + echo "default via 2001:db8::1 dev eth0" +fi +` + if err := os.WriteFile(ipPath, []byte(ipScript), 0755); err != nil { + t.Fatal(err) + } + + initScript := fmt.Sprintf(` +%[4]s "$1" "$2" +mkdir -p %[1]s +if [ -f %[2]s/%[3]s ]; then + cp %[2]s/%[3]s %[1]s/%[3]s +else + touch %[1]s/%[3]s +fi +GW4=$(ip -4 route show default 2>/dev/null | awk '{print $3}' | head -n1) +if [ -n "$GW4" ]; then + printf '\nrouter static\n address-family ipv4 unicast\n 0.0.0.0/0 %%s\n !\n!\n' "$GW4" >> %[1]s/%[3]s +fi +GW6=$(ip -6 route show default 2>/dev/null | awk '{print $3}' | head -n1) +if [ -n "$GW6" ]; then + printf '\nrouter static\n address-family ipv6 unicast\n ::/0 %%s\n !\n!\n' "$GW6" >> %[1]s/%[3]s +fi +`, dstDir, srcDir, configFile, entrypointPath) + + cmd := exec.Command("/bin/sh", "-c", initScript, "init", "2", "0") + cmd.Env = append(os.Environ(), "PATH="+binDir+":"+os.Getenv("PATH")) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("script execution failed: %v, output: %s", err, string(out)) + } + + dstFile := filepath.Join(dstDir, configFile) + content, err := os.ReadFile(dstFile) if err != nil { - t.Fatalf("failed to get created pod: %v", err) + t.Fatalf("failed to read generated config: %v", err) } - if len(pod.Spec.InitContainers) != 1 { - t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + + got := string(content) + wantContains := []string{ + "hostname xrd", + "router static\n address-family ipv4 unicast\n 0.0.0.0/0 10.244.0.1\n !\n!\n", + "router static\n address-family ipv6 unicast\n ::/0 2001:db8::1\n !\n!\n", } - if len(pod.Spec.InitContainers[0].VolumeMounts) != 2 { - t.Fatalf("expected 2 volume mounts in init container, got %d", len(pod.Spec.InitContainers[0].VolumeMounts)) + for _, want := range wantContains { + if !strings.Contains(got, want) { + t.Errorf("generated config missing %q\nGot:\n%s", want, got) + } } } diff --git a/topo/node/juniper/juniper.go b/topo/node/juniper/juniper.go index 29aa07ad6..63169aab7 100644 --- a/topo/node/juniper/juniper.go +++ b/topo/node/juniper/juniper.go @@ -663,11 +663,9 @@ fi pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, vm) } - if pb.Config.ConfigData != nil { - vol, err := n.CreateConfig(ctx) - if err != nil { - return err - } + if vol, err := n.CreateConfig(ctx); err != nil { + return err + } else if vol != nil { vol.Name = "startup-config-src-volume" pod.Spec.Volumes = append(pod.Spec.Volumes, *vol) initVolumeMounts = append(initVolumeMounts, corev1.VolumeMount{ diff --git a/topo/node/juniper/juniper_test.go b/topo/node/juniper/juniper_test.go index 3d1e77510..6b340a59b 100644 --- a/topo/node/juniper/juniper_test.go +++ b/topo/node/juniper/juniper_test.go @@ -10,7 +10,10 @@ import ( "fmt" "io" "os" + "os/exec" + "path/filepath" "regexp" + "strings" "testing" "time" @@ -926,41 +929,186 @@ func TestValidCertNameRegexp(t *testing.T) { } func TestCreate(t *testing.T) { - ki := fake.NewSimpleClientset() - ni := &node.Impl{ - Namespace: "test", - KubeClient: ki, - Proto: &tpb.Node{ - Name: "ncptx", - Model: "ncptx", - Vendor: tpb.Vendor_JUNIPER, - Config: &tpb.Config{ + tests := []struct { + desc string + configData []byte + wantInitCommand []string + wantInitArgsLen int + wantInitMountsLen int + wantMainMountsLen int + wantInitScriptSub []string + wantErr bool + }{ + { + desc: "cPTX with config data", + configData: []byte("set system host-name ncptx"), + wantInitCommand: []string{"/bin/sh", "-c"}, + wantInitArgsLen: 4, // script, "init", num_intfs, sleep + wantInitMountsLen: 2, // /config-dst and /config-src + wantMainMountsLen: 5, // 4 base mounts (/run, /tmp, /dev/shm, /sys/fs/cgroup) + config mount + wantInitScriptSub: []string{"/entrypoint.sh", "re0:mgmt-0 unit 0 family inet", "routing-options static route 0.0.0.0/0", "/config-dst/juniper.conf"}, + }, + { + desc: "cPTX without config data", + configData: nil, + wantInitCommand: []string{"/bin/sh", "-c"}, + wantInitArgsLen: 4, + wantInitMountsLen: 1, // only /config-dst + wantMainMountsLen: 5, + wantInitScriptSub: []string{"/entrypoint.sh", "re0:mgmt-0 unit 0 family inet", "routing-options static route 0.0.0.0/0"}, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ki := fake.NewSimpleClientset() + cfg := &tpb.Config{ ConfigFile: "juniper.conf", ConfigPath: "/home/evo/configdisk", - ConfigData: &tpb.Config_Data{ - Data: []byte("set system host-name ncptx"), + } + if tt.configData != nil { + cfg.ConfigData = &tpb.Config_Data{Data: tt.configData} + } + ni := &node.Impl{ + Namespace: "test", + KubeClient: ki, + Proto: &tpb.Node{ + Name: "ncptx", + Model: "ncptx", + Vendor: tpb.Vendor_JUNIPER, + Config: cfg, + Interfaces: map[string]*tpb.Interface{ + "eth1": {Name: "et-0/0/0:0"}, + }, }, - }, - Interfaces: map[string]*tpb.Interface{ - "eth1": {Name: "et-0/0/0:0"}, - }, - }, + } + n, err := New(ni) + if err != nil { + t.Fatalf("New() unexpected error = %v", err) + } + if err := n.Create(context.Background()); (err != nil) != tt.wantErr { + t.Fatalf("Create() unexpected error = %v, wantErr = %v", err, tt.wantErr) + } + pod, err := ki.CoreV1().Pods("test").Get(context.Background(), "ncptx", metav1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get created pod: %v", err) + } + if len(pod.Spec.InitContainers) != 1 { + t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + } + initC := pod.Spec.InitContainers[0] + if diff := cmp.Diff(tt.wantInitCommand, initC.Command); diff != "" { + t.Errorf("init container command diff (-want +got):\n%s", diff) + } + if len(initC.Args) != tt.wantInitArgsLen { + t.Errorf("init container args len = %d, want %d", len(initC.Args), tt.wantInitArgsLen) + } + if len(initC.VolumeMounts) != tt.wantInitMountsLen { + t.Errorf("init container volume mounts len = %d, want %d", len(initC.VolumeMounts), tt.wantInitMountsLen) + } + if len(pod.Spec.Containers[0].VolumeMounts) != tt.wantMainMountsLen { + t.Errorf("main container volume mounts len = %d, want %d", len(pod.Spec.Containers[0].VolumeMounts), tt.wantMainMountsLen) + } + for _, sub := range tt.wantInitScriptSub { + if !strings.Contains(initC.Args[0], sub) { + t.Errorf("init container script missing expected substring %q", sub) + } + } + }) } - n, err := New(ni) - if err != nil { - t.Fatalf("New() unexpected error = %v", err) +} + +func TestJuniperInitScriptExecution(t *testing.T) { + tmpDir := t.TempDir() + srcDir := filepath.Join(tmpDir, "config-src") + dstDir := filepath.Join(tmpDir, "config-dst") + binDir := filepath.Join(tmpDir, "bin") + if err := os.MkdirAll(srcDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(binDir, 0755); err != nil { + t.Fatal(err) + } + + configFile := "juniper.conf" + srcFile := filepath.Join(srcDir, configFile) + if err := os.WriteFile(srcFile, []byte("set system host-name ncptx\n"), 0644); err != nil { + t.Fatal(err) + } + + // Create fake entrypoint.sh in binDir + entrypointPath := filepath.Join(binDir, "entrypoint.sh") + if err := os.WriteFile(entrypointPath, []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil { + t.Fatal(err) + } + + // Create fake ip command in binDir + ipPath := filepath.Join(binDir, "ip") + ipScript := `#!/bin/sh +if [ "$1" = "-4" ] && [ "$2" = "addr" ]; then + echo " inet 10.244.0.15/24 scope global eth0" +elif [ "$1" = "-4" ] && [ "$2" = "route" ]; then + echo "default via 10.244.0.1 dev eth0" +elif [ "$1" = "-6" ] && [ "$2" = "addr" ]; then + echo " inet6 2001:db8::15/64 scope global" +elif [ "$1" = "-6" ] && [ "$2" = "route" ]; then + echo "default via 2001:db8::1 dev eth0" +fi +` + if err := os.WriteFile(ipPath, []byte(ipScript), 0755); err != nil { + t.Fatal(err) } - if err := n.Create(context.Background()); err != nil { - t.Fatalf("Create() unexpected error = %v", err) + + initScript := fmt.Sprintf(` +%[4]s "$1" "$2" +mkdir -p %[1]s +if [ -f %[2]s/%[3]s ]; then + cp %[2]s/%[3]s %[1]s/%[3]s +else + touch %[1]s/%[3]s +fi +IP4=$(ip -4 addr show dev eth0 2>/dev/null | awk '/inet /{print $2}' | head -n1) +GW4=$(ip -4 route show default 2>/dev/null | awk '{print $3}' | head -n1) +if [ -n "$IP4" ]; then + printf '\nset interfaces re0:mgmt-0 unit 0 family inet address %%s\n' "$IP4" >> %[1]s/%[3]s +fi +if [ -n "$GW4" ]; then + printf 'set routing-options static route 0.0.0.0/0 next-hop %%s\n' "$GW4" >> %[1]s/%[3]s +fi +IP6=$(ip -6 addr show dev eth0 2>/dev/null | awk '/inet6 /{print $2}' | grep -v '^fe80' | head -n1) +GW6=$(ip -6 route show default 2>/dev/null | awk '{print $3}' | head -n1) +if [ -n "$IP6" ]; then + printf '\nset interfaces re0:mgmt-0 unit 0 family inet6 address %%s\n' "$IP6" >> %[1]s/%[3]s +fi +if [ -n "$GW6" ]; then + printf 'set routing-options rib inet6.0 static route ::/0 next-hop %%s\n' "$GW6" >> %[1]s/%[3]s +fi +`, dstDir, srcDir, configFile, entrypointPath) + + cmd := exec.Command("/bin/sh", "-c", initScript, "init", "2", "0") + cmd.Env = append(os.Environ(), "PATH="+binDir+":"+os.Getenv("PATH")) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("script execution failed: %v, output: %s", err, string(out)) } - pod, err := ki.CoreV1().Pods("test").Get(context.Background(), "ncptx", metav1.GetOptions{}) + + dstFile := filepath.Join(dstDir, configFile) + content, err := os.ReadFile(dstFile) if err != nil { - t.Fatalf("failed to get created pod: %v", err) + t.Fatalf("failed to read generated config: %v", err) } - if len(pod.Spec.InitContainers) != 1 { - t.Fatalf("expected 1 init container, got %d", len(pod.Spec.InitContainers)) + + got := string(content) + wantContains := []string{ + "set system host-name ncptx", + "set interfaces re0:mgmt-0 unit 0 family inet address 10.244.0.15/24", + "set routing-options static route 0.0.0.0/0 next-hop 10.244.0.1", + "set interfaces re0:mgmt-0 unit 0 family inet6 address 2001:db8::15/64", + "set routing-options rib inet6.0 static route ::/0 next-hop 2001:db8::1", } - if len(pod.Spec.InitContainers[0].VolumeMounts) != 2 { - t.Fatalf("expected 2 volume mounts in init container, got %d", len(pod.Spec.InitContainers[0].VolumeMounts)) + for _, want := range wantContains { + if !strings.Contains(got, want) { + t.Errorf("generated config missing %q\nGot:\n%s", want, got) + } } } diff --git a/topo/node/node.go b/topo/node/node.go index df40f565e..7ad0de437 100644 --- a/topo/node/node.go +++ b/topo/node/node.go @@ -308,10 +308,19 @@ func convertSysctlNameToProcSysPath(sysctlName string) string { } func (n *Impl) readConfig() ([]byte, error) { + if n.Proto == nil || n.Proto.Config == nil { + return nil, nil + } switch v := n.Proto.Config.GetConfigData().(type) { case *tpb.Config_File: + if v == nil { + return nil, nil + } return os.ReadFile(filepath.Join(n.BasePath, v.File)) case *tpb.Config_Data: + if v == nil { + return nil, nil + } return v.Data, nil case nil: return nil, nil From b5dd6e7a674fc02b91da7ff6fbfa946a8c76012c Mon Sep 17 00:00:00 2001 From: Kris Raney Date: Sat, 15 Aug 2026 04:37:13 +0000 Subject: [PATCH 3/7] Need the default config included if not overridden --- topo/node/juniper/juniper.go | 8 +++++- topo/node/juniper/juniper_test.go | 42 ++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/topo/node/juniper/juniper.go b/topo/node/juniper/juniper.go index 63169aab7..57bfc4a45 100644 --- a/topo/node/juniper/juniper.go +++ b/topo/node/juniper/juniper.go @@ -622,7 +622,13 @@ mkdir -p %[1]s if [ -f %[2]s/%[3]s ]; then cp %[2]s/%[3]s %[1]s/%[3]s else - touch %[1]s/%[3]s + cat << 'EOF' > %[1]s/%[3]s +set system root-authentication encrypted-password "$6$7uA5z8vs$cmHIvL0aLU4ioWAHPR0PLeU/mJj.JO/5pQVQoqRlInK3fJNTLYLhwiDi.Q6gHhltSB3S1P/.raEsuDSH7akcJ/" +set system services ssh root-login allow +set system syslog file interactive-commands interactive-commands any +set system syslog file messages any notice +set system syslog file messages authorization info +EOF fi IP4=$(ip -4 addr show dev eth0 2>/dev/null | awk '/inet /{print $2}' | head -n1) GW4=$(ip -4 route show default 2>/dev/null | awk '{print $3}' | head -n1) diff --git a/topo/node/juniper/juniper_test.go b/topo/node/juniper/juniper_test.go index 6b340a59b..a6bbfcab0 100644 --- a/topo/node/juniper/juniper_test.go +++ b/topo/node/juniper/juniper_test.go @@ -1065,7 +1065,13 @@ mkdir -p %[1]s if [ -f %[2]s/%[3]s ]; then cp %[2]s/%[3]s %[1]s/%[3]s else - touch %[1]s/%[3]s + cat << 'EOF' > %[1]s/%[3]s +set system root-authentication encrypted-password "$6$7uA5z8vs$cmHIvL0aLU4ioWAHPR0PLeU/mJj.JO/5pQVQoqRlInK3fJNTLYLhwiDi.Q6gHhltSB3S1P/.raEsuDSH7akcJ/" +set system services ssh root-login allow +set system syslog file interactive-commands interactive-commands any +set system syslog file messages any notice +set system syslog file messages authorization info +EOF fi IP4=$(ip -4 addr show dev eth0 2>/dev/null | awk '/inet /{print $2}' | head -n1) GW4=$(ip -4 route show default 2>/dev/null | awk '{print $3}' | head -n1) @@ -1085,11 +1091,12 @@ if [ -n "$GW6" ]; then fi `, dstDir, srcDir, configFile, entrypointPath) + // Test case 1: With existing source config cmd := exec.Command("/bin/sh", "-c", initScript, "init", "2", "0") cmd.Env = append(os.Environ(), "PATH="+binDir+":"+os.Getenv("PATH")) out, err := cmd.CombinedOutput() if err != nil { - t.Fatalf("script execution failed: %v, output: %s", err, string(out)) + t.Fatalf("script execution with source config failed: %v, output: %s", err, string(out)) } dstFile := filepath.Join(dstDir, configFile) @@ -1108,7 +1115,36 @@ fi } for _, want := range wantContains { if !strings.Contains(got, want) { - t.Errorf("generated config missing %q\nGot:\n%s", want, got) + t.Errorf("generated config with source missing %q\nGot:\n%s", want, got) + } + } + + // Test case 2: Without source config (should generate built-in default config with root password) + os.Remove(srcFile) + os.Remove(dstFile) + + cmd2 := exec.Command("/bin/sh", "-c", initScript, "init", "2", "0") + cmd2.Env = append(os.Environ(), "PATH="+binDir+":"+os.Getenv("PATH")) + out2, err := cmd2.CombinedOutput() + if err != nil { + t.Fatalf("script execution without source config failed: %v, output: %s", err, string(out2)) + } + + content2, err := os.ReadFile(dstFile) + if err != nil { + t.Fatalf("failed to read default generated config: %v", err) + } + + got2 := string(content2) + wantDefaultContains := []string{ + "root-authentication encrypted-password", + "set system services ssh root-login allow", + "set interfaces re0:mgmt-0 unit 0 family inet address 10.244.0.15/24", + "set routing-options static route 0.0.0.0/0 next-hop 10.244.0.1", + } + for _, want := range wantDefaultContains { + if !strings.Contains(got2, want) { + t.Errorf("default generated config missing %q\nGot:\n%s", want, got2) } } } From 49d7fea06d6a4878807a4bdd2b2241988ddcd04e Mon Sep 17 00:00:00 2001 From: Kris Raney Date: Mon, 17 Aug 2026 15:07:38 +0000 Subject: [PATCH 4/7] Use the right cert name if the provided config alters it --- topo/node/juniper/juniper.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/topo/node/juniper/juniper.go b/topo/node/juniper/juniper.go index 57bfc4a45..870f36260 100644 --- a/topo/node/juniper/juniper.go +++ b/topo/node/juniper/juniper.go @@ -195,26 +195,30 @@ func (n *Node) GRPCConfig() []string { port = service.GetInside() } } - log.Infof("gNMI Port %d", port) + certName := "grpc-server-cert" + if selfSigned := n.Proto.GetConfig().GetCert().GetSelfSigned(); selfSigned != nil && selfSigned.GetCertName() != "" { + certName = selfSigned.GetCertName() + } + log.Infof("gNMI Port %d, certName %s", port, certName) return []string{ "set system services http servers server grpc-server-9339", fmt.Sprintf("set system services http servers server grpc-server-9339 port %d", port), "set system services http servers server grpc-server-9339 grpc gnmi", "set system services http servers server grpc-server-9339 grpc gnoi", "set system services http servers server grpc-server-9339 grpc gnsi", - "set system services http servers server grpc-server-9339 tls local-certificate grpc-server-cert", + fmt.Sprintf("set system services http servers server grpc-server-9339 tls local-certificate %s", certName), "set system services http servers server grpc-server-9339 listen-address 0.0.0.0", "set system services http servers server grpc-server-9339 grpc all-grpc max-connections 300", "set system services http servers server grpc-server-9340", "set system services http servers server grpc-server-9340 port 9340", "set system services http servers server grpc-server-9340 grpc gribi", - "set system services http servers server grpc-server-9340 tls local-certificate grpc-server-cert", + fmt.Sprintf("set system services http servers server grpc-server-9340 tls local-certificate %s", certName), "set system services http servers server grpc-server-9340 listen-address 0.0.0.0", "set system services http servers server grpc-server-9340 grpc all-grpc max-connections 300", "set system services http servers server grpc-server-9559", "set system services http servers server grpc-server-9559 port 9559", "set system services http servers server grpc-server-9559 grpc p4", - "set system services http servers server grpc-server-9559 tls local-certificate grpc-server-cert", + fmt.Sprintf("set system services http servers server grpc-server-9559 tls local-certificate %s", certName), "set system services http servers server grpc-server-9559 listen-address 0.0.0.0", "set system services http servers server grpc-server-9559 grpc all-grpc max-connections 300", "commit", From c928c738dee1be6bcbe8c615ab2ce06612deae7b Mon Sep 17 00:00:00 2001 From: Kris Raney Date: Mon, 17 Aug 2026 15:12:13 +0000 Subject: [PATCH 5/7] Fix lint warnings --- topo/node/cisco/cisco.go | 2 +- topo/node/juniper/juniper_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/topo/node/cisco/cisco.go b/topo/node/cisco/cisco.go index 48a1ea5da..9bd20c448 100644 --- a/topo/node/cisco/cisco.go +++ b/topo/node/cisco/cisco.go @@ -331,7 +331,7 @@ fi MountPath: pb.Config.ConfigPath + "/" + pb.Config.ConfigFile, ReadOnly: true, } - if vol.VolumeSource.ConfigMap != nil { + if vol.ConfigMap != nil { vm.SubPath = pb.Config.ConfigFile } for i, c := range pod.Spec.Containers { diff --git a/topo/node/juniper/juniper_test.go b/topo/node/juniper/juniper_test.go index a6bbfcab0..80045cc1e 100644 --- a/topo/node/juniper/juniper_test.go +++ b/topo/node/juniper/juniper_test.go @@ -1120,8 +1120,8 @@ fi } // Test case 2: Without source config (should generate built-in default config with root password) - os.Remove(srcFile) - os.Remove(dstFile) + _ = os.Remove(srcFile) + _ = os.Remove(dstFile) cmd2 := exec.Command("/bin/sh", "-c", initScript, "init", "2", "0") cmd2.Env = append(os.Environ(), "PATH="+binDir+":"+os.Getenv("PATH")) From 624e2e54ba16ae94308a33e06b24286dbfce50ae Mon Sep 17 00:00:00 2001 From: Kris Raney Date: Mon, 17 Aug 2026 18:03:28 +0000 Subject: [PATCH 6/7] Account for placeholder IP string FXP0ADDR in config gen --- topo/node/juniper/juniper.go | 63 ++++++++++++++++++---- topo/node/juniper/juniper_test.go | 88 ++++++++++++++++++++++++------- 2 files changed, 122 insertions(+), 29 deletions(-) diff --git a/topo/node/juniper/juniper.go b/topo/node/juniper/juniper.go index 870f36260..3cd91c3ba 100644 --- a/topo/node/juniper/juniper.go +++ b/topo/node/juniper/juniper.go @@ -627,28 +627,73 @@ if [ -f %[2]s/%[3]s ]; then cp %[2]s/%[3]s %[1]s/%[3]s else cat << 'EOF' > %[1]s/%[3]s -set system root-authentication encrypted-password "$6$7uA5z8vs$cmHIvL0aLU4ioWAHPR0PLeU/mJj.JO/5pQVQoqRlInK3fJNTLYLhwiDi.Q6gHhltSB3S1P/.raEsuDSH7akcJ/" -set system services ssh root-login allow -set system syslog file interactive-commands interactive-commands any -set system syslog file messages any notice -set system syslog file messages authorization info +system { + root-authentication { + encrypted-password "$6$7uA5z8vs$cmHIvL0aLU4ioWAHPR0PLeU/mJj.JO/5pQVQoqRlInK3fJNTLYLhwiDi.Q6gHhltSB3S1P/.raEsuDSH7akcJ/"; + } + services { + ssh { + root-login allow; + } + } +} EOF fi IP4=$(ip -4 addr show dev eth0 2>/dev/null | awk '/inet /{print $2}' | head -n1) GW4=$(ip -4 route show default 2>/dev/null | awk '{print $3}' | head -n1) if [ -n "$IP4" ]; then - printf '\nset interfaces re0:mgmt-0 unit 0 family inet address %%s\n' "$IP4" >> %[1]s/%[3]s + if grep -q "FXP0ADDR" %[1]s/%[3]s; then + sed -i "s|FXP0ADDR|$IP4|g" %[1]s/%[3]s + elif ! grep -q "re0:mgmt-0" %[1]s/%[3]s; then + cat << EOF >> %[1]s/%[3]s +interfaces { + re0:mgmt-0 { + unit 0 { + family inet { + address $IP4; + } + } + } +} +EOF + fi fi if [ -n "$GW4" ]; then - printf 'set routing-options static route 0.0.0.0/0 next-hop %%s\n' "$GW4" >> %[1]s/%[3]s + cat << EOF >> %[1]s/%[3]s +routing-options { + static { + route 0.0.0.0/0 next-hop $GW4; + } +} +EOF fi IP6=$(ip -6 addr show dev eth0 2>/dev/null | awk '/inet6 /{print $2}' | grep -v '^fe80' | head -n1) GW6=$(ip -6 route show default 2>/dev/null | awk '{print $3}' | head -n1) if [ -n "$IP6" ]; then - printf '\nset interfaces re0:mgmt-0 unit 0 family inet6 address %%s\n' "$IP6" >> %[1]s/%[3]s + if ! grep -q "family inet6" %[1]s/%[3]s; then + cat << EOF >> %[1]s/%[3]s +interfaces { + re0:mgmt-0 { + unit 0 { + family inet6 { + address $IP6; + } + } + } +} +EOF + fi fi if [ -n "$GW6" ]; then - printf 'set routing-options rib inet6.0 static route ::/0 next-hop %%s\n' "$GW6" >> %[1]s/%[3]s + cat << EOF >> %[1]s/%[3]s +routing-options { + rib inet6.0 { + static { + route ::/0 next-hop $GW6; + } + } +} +EOF fi `, configDstPath, configSrcPath, configFile) diff --git a/topo/node/juniper/juniper_test.go b/topo/node/juniper/juniper_test.go index 80045cc1e..87cab23ad 100644 --- a/topo/node/juniper/juniper_test.go +++ b/topo/node/juniper/juniper_test.go @@ -946,7 +946,7 @@ func TestCreate(t *testing.T) { wantInitArgsLen: 4, // script, "init", num_intfs, sleep wantInitMountsLen: 2, // /config-dst and /config-src wantMainMountsLen: 5, // 4 base mounts (/run, /tmp, /dev/shm, /sys/fs/cgroup) + config mount - wantInitScriptSub: []string{"/entrypoint.sh", "re0:mgmt-0 unit 0 family inet", "routing-options static route 0.0.0.0/0", "/config-dst/juniper.conf"}, + wantInitScriptSub: []string{"/entrypoint.sh", "re0:mgmt-0", "route 0.0.0.0/0 next-hop", "/config-dst/juniper.conf"}, }, { desc: "cPTX without config data", @@ -955,7 +955,7 @@ func TestCreate(t *testing.T) { wantInitArgsLen: 4, wantInitMountsLen: 1, // only /config-dst wantMainMountsLen: 5, - wantInitScriptSub: []string{"/entrypoint.sh", "re0:mgmt-0 unit 0 family inet", "routing-options static route 0.0.0.0/0"}, + wantInitScriptSub: []string{"/entrypoint.sh", "re0:mgmt-0", "route 0.0.0.0/0 next-hop"}, }, } @@ -1032,7 +1032,7 @@ func TestJuniperInitScriptExecution(t *testing.T) { configFile := "juniper.conf" srcFile := filepath.Join(srcDir, configFile) - if err := os.WriteFile(srcFile, []byte("set system host-name ncptx\n"), 0644); err != nil { + if err := os.WriteFile(srcFile, []byte("set system host-name ncptx\naddress FXP0ADDR;\n"), 0644); err != nil { t.Fatal(err) } @@ -1066,28 +1066,73 @@ if [ -f %[2]s/%[3]s ]; then cp %[2]s/%[3]s %[1]s/%[3]s else cat << 'EOF' > %[1]s/%[3]s -set system root-authentication encrypted-password "$6$7uA5z8vs$cmHIvL0aLU4ioWAHPR0PLeU/mJj.JO/5pQVQoqRlInK3fJNTLYLhwiDi.Q6gHhltSB3S1P/.raEsuDSH7akcJ/" -set system services ssh root-login allow -set system syslog file interactive-commands interactive-commands any -set system syslog file messages any notice -set system syslog file messages authorization info +system { + root-authentication { + encrypted-password "$6$7uA5z8vs$cmHIvL0aLU4ioWAHPR0PLeU/mJj.JO/5pQVQoqRlInK3fJNTLYLhwiDi.Q6gHhltSB3S1P/.raEsuDSH7akcJ/"; + } + services { + ssh { + root-login allow; + } + } +} EOF fi IP4=$(ip -4 addr show dev eth0 2>/dev/null | awk '/inet /{print $2}' | head -n1) GW4=$(ip -4 route show default 2>/dev/null | awk '{print $3}' | head -n1) if [ -n "$IP4" ]; then - printf '\nset interfaces re0:mgmt-0 unit 0 family inet address %%s\n' "$IP4" >> %[1]s/%[3]s + if grep -q "FXP0ADDR" %[1]s/%[3]s; then + sed -i "s|FXP0ADDR|$IP4|g" %[1]s/%[3]s + elif ! grep -q "re0:mgmt-0" %[1]s/%[3]s; then + cat << EOF >> %[1]s/%[3]s +interfaces { + re0:mgmt-0 { + unit 0 { + family inet { + address $IP4; + } + } + } +} +EOF + fi fi if [ -n "$GW4" ]; then - printf 'set routing-options static route 0.0.0.0/0 next-hop %%s\n' "$GW4" >> %[1]s/%[3]s + cat << EOF >> %[1]s/%[3]s +routing-options { + static { + route 0.0.0.0/0 next-hop $GW4; + } +} +EOF fi IP6=$(ip -6 addr show dev eth0 2>/dev/null | awk '/inet6 /{print $2}' | grep -v '^fe80' | head -n1) GW6=$(ip -6 route show default 2>/dev/null | awk '{print $3}' | head -n1) if [ -n "$IP6" ]; then - printf '\nset interfaces re0:mgmt-0 unit 0 family inet6 address %%s\n' "$IP6" >> %[1]s/%[3]s + if ! grep -q "family inet6" %[1]s/%[3]s; then + cat << EOF >> %[1]s/%[3]s +interfaces { + re0:mgmt-0 { + unit 0 { + family inet6 { + address $IP6; + } + } + } +} +EOF + fi fi if [ -n "$GW6" ]; then - printf 'set routing-options rib inet6.0 static route ::/0 next-hop %%s\n' "$GW6" >> %[1]s/%[3]s + cat << EOF >> %[1]s/%[3]s +routing-options { + rib inet6.0 { + static { + route ::/0 next-hop $GW6; + } + } +} +EOF fi `, dstDir, srcDir, configFile, entrypointPath) @@ -1106,12 +1151,15 @@ fi } got := string(content) + if strings.Contains(got, "FXP0ADDR") { + t.Errorf("generated config contains invalid FXP0ADDR placeholder line\nGot:\n%s", got) + } wantContains := []string{ "set system host-name ncptx", - "set interfaces re0:mgmt-0 unit 0 family inet address 10.244.0.15/24", - "set routing-options static route 0.0.0.0/0 next-hop 10.244.0.1", - "set interfaces re0:mgmt-0 unit 0 family inet6 address 2001:db8::15/64", - "set routing-options rib inet6.0 static route ::/0 next-hop 2001:db8::1", + "address 10.244.0.15/24;", + "route 0.0.0.0/0 next-hop 10.244.0.1;", + "address 2001:db8::15/64;", + "route ::/0 next-hop 2001:db8::1;", } for _, want := range wantContains { if !strings.Contains(got, want) { @@ -1137,10 +1185,10 @@ fi got2 := string(content2) wantDefaultContains := []string{ - "root-authentication encrypted-password", - "set system services ssh root-login allow", - "set interfaces re0:mgmt-0 unit 0 family inet address 10.244.0.15/24", - "set routing-options static route 0.0.0.0/0 next-hop 10.244.0.1", + "root-authentication", + "encrypted-password", + "address 10.244.0.15/24;", + "route 0.0.0.0/0 next-hop 10.244.0.1;", } for _, want := range wantDefaultContains { if !strings.Contains(got2, want) { From 077ae482053b00eb0a3680a3da01242818784355 Mon Sep 17 00:00:00 2001 From: Kris Raney Date: Tue, 18 Aug 2026 02:01:04 +0000 Subject: [PATCH 7/7] Fix broken rate default check These variables are initialized to a non-zero default, so the check was never successful --- deploy/deploy.go | 4 ++-- third_party/meshnet/daemon/meshnet/meshnet.go | 4 ++-- topo/topo.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy/deploy.go b/deploy/deploy.go index 77c3b1f39..a6e65b1b7 100644 --- a/deploy/deploy.go +++ b/deploy/deploy.go @@ -207,10 +207,10 @@ func (d *Deployment) Deploy(ctx context.Context, kubecfg string) (rerr error) { if err != nil { return fmt.Errorf("failed to create k8s config: %w", err) } - if rCfg.QPS == 0 { + if rCfg.QPS < 100 { rCfg.QPS = 100 } - if rCfg.Burst == 0 { + if rCfg.Burst < 200 { rCfg.Burst = 200 } kClient, err := kubernetes.NewForConfig(rCfg) diff --git a/third_party/meshnet/daemon/meshnet/meshnet.go b/third_party/meshnet/daemon/meshnet/meshnet.go index e31db49d6..29d388449 100644 --- a/third_party/meshnet/daemon/meshnet/meshnet.go +++ b/third_party/meshnet/daemon/meshnet/meshnet.go @@ -74,10 +74,10 @@ func restConfig() (*rest.Config, error) { return nil, err } } - if rCfg.QPS == 0 { + if rCfg.QPS < 100 { rCfg.QPS = 100 } - if rCfg.Burst == 0 { + if rCfg.Burst < 200 { rCfg.Burst = 200 } return rCfg, nil diff --git a/topo/topo.go b/topo/topo.go index e3525f140..05c91d734 100644 --- a/topo/topo.go +++ b/topo/topo.go @@ -186,10 +186,10 @@ func New(topo *tpb.Topology, opts ...Option) (*Manager, error) { } m.rCfg = rCfg } - if m.rCfg.QPS == 0 { + if m.rCfg.QPS < 100 { m.rCfg.QPS = 100 } - if m.rCfg.Burst == 0 { + if m.rCfg.Burst < 200 { m.rCfg.Burst = 200 } if m.kClient == nil {