diff --git a/examples/ciena/README.md b/examples/ciena/README.md new file mode 100644 index 00000000..9b22c3aa --- /dev/null +++ b/examples/ciena/README.md @@ -0,0 +1,104 @@ +# How to configure Ciena simulators in KNE + +## Interface naming +- `eth0` - Node management interface + +### For saos +- `1` - First dataplane interface +- `X` - Subsequent dataplane interfaces will count onwards from 1. For example, the third dataplane interface will be `3` + +### For waverouter +Waverouter port numbering format is: `//`. In the example below, 1/5/1, this is housing 1, slot 5, port 1 +- `1/5/1` - First dataplane interface +- `1/5/X` - Subsequent dataplane interfaces will count onwards from 1. For example, the third dataplane interface will be `1/5/3` + +### Notes: +- You can also use interface aliases of `ethX` (count onwards from 1) for both saos and waverouter +- We only support one waverouter interface box (using the wr-qbox type in JSON) at this time + +### [wr13_example.json](./wr13_example.json) +```json +{ + "WR1": { + "1": { + "type": "wr13", + "7": { + "type": "wr-ctm" + }, + "5": { + "type": "wr-qbox" + } + } + } +} +``` + +## [saos.pbtxt topology](./saos.pbtxt) +This topology includes 2 saos which has 2 connections, saos-1 1----1 saos-2, saos-1 2----2 saos-2 +```yaml +name: "saos-example" +nodes: { + name: "saos-1" + vendor: CIENA + model: "5132" + # when `image` is not specified under `config`, the "artifactory.ciena.com/psa/saos-containerlab:latest" container image is used by default +} +nodes: { + name: "saos-2" + vendor: CIENA + model: "5132" + # when `image` is not specified under `config`, the "artifactory.ciena.com/psa/saos-containerlab:latest" container image is used by default +} +links: { + a_node: "saos-1" + a_int: "1" + z_node: "saos-2" + z_int: "1" +} +links: { + a_node: "saos-1" + a_int: "2" + z_node: "saos-2" + z_int: "2" +} +``` + +## [waverouter.pbtxt topology](./waverouter.pbtxt) +This topology includes one saos and one waverouter which has 2 connections, saos-1 1----1/5/1 wr-1, saos-1 2----1/5/2 wr-1 +* Waverouter model requires an additional file (wr13_example.json) to describe the internal system topology +```yaml +name: "waverouter-saos-example" +nodes: { + name: "wr-1" + vendor: CIENA + model: "waverouter" + config: { + # when `image` is not specified under `config`, the "artifactory.ciena.com/psa/rw-containerlab:latest" container image is used by default + vendor_data { + [type.googleapis.com/ciena.CienaConfig] { + system_equipment: { + equipment_json: "wr13_example.json" + } + } + } + } +} +nodes: { + name: "saos-1" + vendor: CIENA + model: "5132" + # when `image` is not specified under `config`, the "artifactory.ciena.com/psa/saos-containerlab:latest" container image is used by default +} +links: { + a_node: "saos-1" + a_int: "1" + z_node: "wr-1" + z_int: "1/5/1" +} +links: { + a_node: "saos-1" + a_int: "2" + z_node: "wr-1" + z_int: "1/5/2" +} +``` \ No newline at end of file diff --git a/examples/ciena/saos.pbtxt b/examples/ciena/saos.pbtxt new file mode 100644 index 00000000..961863a6 --- /dev/null +++ b/examples/ciena/saos.pbtxt @@ -0,0 +1,25 @@ +name: "saos-example" +nodes: { + name: "saos-1" + vendor: CIENA + model: "5132" + # when `image` is not specified under `config`, the "artifactory.ciena.com/psa/saos-containerlab:latest" container image is used by default +} +nodes: { + name: "saos-2" + vendor: CIENA + model: "5132" + # when `image` is not specified under `config`, the "artifactory.ciena.com/psa/saos-containerlab:latest" container image is used by default +} +links: { + a_node: "saos-1" + a_int: "1" + z_node: "saos-2" + z_int: "1" +} +links: { + a_node: "saos-1" + a_int: "2" + z_node: "saos-2" + z_int: "2" +} \ No newline at end of file diff --git a/examples/ciena/waverouter.pbtxt b/examples/ciena/waverouter.pbtxt new file mode 100644 index 00000000..38c88356 --- /dev/null +++ b/examples/ciena/waverouter.pbtxt @@ -0,0 +1,34 @@ +name: "waverouter-saos-example" +nodes: { + name: "wr-1" + vendor: CIENA + model: "waverouter" + config: { + # when `image` is not specified under `config`, the "artifactory.ciena.com/psa/rw-containerlab:latest" container image is used by default + vendor_data { + [type.googleapis.com/ciena.CienaConfig] { + system_equipment: { + equipment_json: "wr13_example.json" + } + } + } + } +} +nodes: { + name: "saos-1" + vendor: CIENA + model: "5132" + # when `image` is not specified under `config`, the "artifactory.ciena.com/psa/saos-containerlab:latest" container image is used by default +} +links: { + a_node: "saos-1" + a_int: "1" + z_node: "wr-1" + z_int: "1/5/1" +} +links: { + a_node: "saos-1" + a_int: "2" + z_node: "wr-1" + z_int: "1/5/2" +} \ No newline at end of file diff --git a/examples/ciena/wr13_example.json b/examples/ciena/wr13_example.json new file mode 100644 index 00000000..88933c10 --- /dev/null +++ b/examples/ciena/wr13_example.json @@ -0,0 +1,13 @@ +{ + "WR1": { + "1": { + "type": "wr13", + "7": { + "type": "wr-ctm" + }, + "5": { + "type": "wr-qbox" + } + } + } +} \ No newline at end of file diff --git a/proto/ciena.proto b/proto/ciena.proto new file mode 100644 index 00000000..c65673a5 --- /dev/null +++ b/proto/ciena.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package ciena; + +option go_package = "github.com/openconfig/kne/proto/ciena"; + +message SystemEquipment { + // Mount point for the files inside the pod. + string mount_dir = 1; + string equipment_json = 2; +} + +// Ciena specific vendor data for KNE +message CienaConfig { + SystemEquipment system_equipment = 1; +} diff --git a/proto/ciena/ciena.pb.go b/proto/ciena/ciena.pb.go new file mode 100644 index 00000000..f680b861 --- /dev/null +++ b/proto/ciena/ciena.pb.go @@ -0,0 +1,222 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v6.31.1 +// source: ciena.proto + +package ciena + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SystemEquipment struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Mount point for the files inside the pod. + MountDir string `protobuf:"bytes,1,opt,name=mount_dir,json=mountDir,proto3" json:"mount_dir,omitempty"` + EquipmentJson string `protobuf:"bytes,2,opt,name=equipment_json,json=equipmentJson,proto3" json:"equipment_json,omitempty"` +} + +func (x *SystemEquipment) Reset() { + *x = SystemEquipment{} + if protoimpl.UnsafeEnabled { + mi := &file_ciena_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SystemEquipment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SystemEquipment) ProtoMessage() {} + +func (x *SystemEquipment) ProtoReflect() protoreflect.Message { + mi := &file_ciena_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SystemEquipment.ProtoReflect.Descriptor instead. +func (*SystemEquipment) Descriptor() ([]byte, []int) { + return file_ciena_proto_rawDescGZIP(), []int{0} +} + +func (x *SystemEquipment) GetMountDir() string { + if x != nil { + return x.MountDir + } + return "" +} + +func (x *SystemEquipment) GetEquipmentJson() string { + if x != nil { + return x.EquipmentJson + } + return "" +} + +// Ciena specific vendor data for KNE +type CienaConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SystemEquipment *SystemEquipment `protobuf:"bytes,1,opt,name=system_equipment,json=systemEquipment,proto3" json:"system_equipment,omitempty"` +} + +func (x *CienaConfig) Reset() { + *x = CienaConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_ciena_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CienaConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CienaConfig) ProtoMessage() {} + +func (x *CienaConfig) ProtoReflect() protoreflect.Message { + mi := &file_ciena_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CienaConfig.ProtoReflect.Descriptor instead. +func (*CienaConfig) Descriptor() ([]byte, []int) { + return file_ciena_proto_rawDescGZIP(), []int{1} +} + +func (x *CienaConfig) GetSystemEquipment() *SystemEquipment { + if x != nil { + return x.SystemEquipment + } + return nil +} + +var File_ciena_proto protoreflect.FileDescriptor + +var file_ciena_proto_rawDesc = []byte{ + 0x0a, 0x0b, 0x63, 0x69, 0x65, 0x6e, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x63, + 0x69, 0x65, 0x6e, 0x61, 0x22, 0x55, 0x0a, 0x0f, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x71, + 0x75, 0x69, 0x70, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x5f, 0x64, 0x69, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x44, 0x69, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x71, 0x75, 0x69, 0x70, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x71, + 0x75, 0x69, 0x70, 0x6d, 0x65, 0x6e, 0x74, 0x4a, 0x73, 0x6f, 0x6e, 0x22, 0x50, 0x0a, 0x0b, 0x43, + 0x69, 0x65, 0x6e, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x41, 0x0a, 0x10, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x65, 0x71, 0x75, 0x69, 0x70, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x69, 0x65, 0x6e, 0x61, 0x2e, 0x53, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x45, 0x71, 0x75, 0x69, 0x70, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0f, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x45, 0x71, 0x75, 0x69, 0x70, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x27, 0x5a, + 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x6b, 0x6e, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x63, 0x69, 0x65, 0x6e, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_ciena_proto_rawDescOnce sync.Once + file_ciena_proto_rawDescData = file_ciena_proto_rawDesc +) + +func file_ciena_proto_rawDescGZIP() []byte { + file_ciena_proto_rawDescOnce.Do(func() { + file_ciena_proto_rawDescData = protoimpl.X.CompressGZIP(file_ciena_proto_rawDescData) + }) + return file_ciena_proto_rawDescData +} + +var file_ciena_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_ciena_proto_goTypes = []interface{}{ + (*SystemEquipment)(nil), // 0: ciena.SystemEquipment + (*CienaConfig)(nil), // 1: ciena.CienaConfig +} +var file_ciena_proto_depIdxs = []int32{ + 0, // 0: ciena.CienaConfig.system_equipment:type_name -> ciena.SystemEquipment + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_ciena_proto_init() } +func file_ciena_proto_init() { + if File_ciena_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_ciena_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SystemEquipment); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ciena_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CienaConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_ciena_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_ciena_proto_goTypes, + DependencyIndexes: file_ciena_proto_depIdxs, + MessageInfos: file_ciena_proto_msgTypes, + }.Build() + File_ciena_proto = out.File + file_ciena_proto_rawDesc = nil + file_ciena_proto_goTypes = nil + file_ciena_proto_depIdxs = nil +} diff --git a/proto/generate.go b/proto/generate.go index e47e9d0b..9899e211 100644 --- a/proto/generate.go +++ b/proto/generate.go @@ -7,3 +7,4 @@ package proto //go:generate protoc --go_out=./wire --go-grpc_out=./wire --go-grpc_opt=paths=source_relative --go_opt=paths=source_relative ./wire.proto //go:generate protoc --go_out=./forward --go-grpc_out=./forward --go-grpc_opt=paths=source_relative --go_opt=paths=source_relative ./forward.proto //go:generate protoc --go_out=./event --go-grpc_out=./event --go-grpc_opt=paths=source_relative --go_opt=paths=source_relative ./event.proto +//go:generate protoc --go_out=./ciena --go_opt=paths=source_relative ./ciena.proto diff --git a/proto/topo.proto b/proto/topo.proto index 4af374c7..052222a3 100644 --- a/proto/topo.proto +++ b/proto/topo.proto @@ -44,6 +44,7 @@ enum Vendor { ALPINE = 11; DRIVENETS = 12; FORWARD = 13; + CIENA = 14; } // Node is a single container inside the topology @@ -65,6 +66,7 @@ message Node { CISCO_XRD = 12; CISCO_E8000 = 13; LEMMING = 14; + CIENA_SAOS = 15; } string name = 1; // Name of the node in the topology. Must be unique. Type type = 2 [deprecated = true]; diff --git a/proto/topo/topo.pb.go b/proto/topo/topo.pb.go index 35537334..6a206190 100644 --- a/proto/topo/topo.pb.go +++ b/proto/topo/topo.pb.go @@ -54,6 +54,7 @@ const ( Vendor_ALPINE Vendor = 11 Vendor_DRIVENETS Vendor = 12 Vendor_FORWARD Vendor = 13 + Vendor_CIENA Vendor = 14 ) // Enum value maps for Vendor. @@ -73,6 +74,7 @@ var ( 11: "ALPINE", 12: "DRIVENETS", 13: "FORWARD", + 14: "CIENA", } Vendor_value = map[string]int32{ "UNKNOWN": 0, @@ -89,6 +91,7 @@ var ( "ALPINE": 11, "DRIVENETS": 12, "FORWARD": 13, + "CIENA": 14, } ) @@ -137,6 +140,7 @@ const ( Node_CISCO_XRD Node_Type = 12 Node_CISCO_E8000 Node_Type = 13 Node_LEMMING Node_Type = 14 + Node_CIENA_SAOS Node_Type = 15 ) // Enum value maps for Node_Type. @@ -157,6 +161,7 @@ var ( 12: "CISCO_XRD", 13: "CISCO_E8000", 14: "LEMMING", + 15: "CIENA_SAOS", } Node_Type_value = map[string]int32{ "UNKNOWN": 0, @@ -174,6 +179,7 @@ var ( "CISCO_XRD": 12, "CISCO_E8000": 13, "LEMMING": 14, + "CIENA_SAOS": 15, } ) @@ -1321,7 +1327,7 @@ var file_topo_proto_rawDesc = []byte{ 0x6f, 0x70, 0x6f, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x05, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x05, 0x6c, 0x69, 0x6e, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x74, 0x6f, 0x70, 0x6f, 0x2e, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x05, 0x6c, 0x69, 0x6e, 0x6b, - 0x73, 0x22, 0x80, 0x08, 0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x73, 0x22, 0x90, 0x08, 0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0f, 0x2e, 0x74, 0x6f, 0x70, 0x6f, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x42, 0x02, 0x18, @@ -1371,7 +1377,7 @@ var file_topo_proto_rawDesc = []byte{ 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x25, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x6f, 0x70, 0x6f, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x22, 0xd8, 0x01, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, + 0x02, 0x38, 0x01, 0x22, 0xe8, 0x01, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x41, 0x52, 0x49, 0x53, 0x54, 0x41, 0x5f, 0x43, 0x45, 0x4f, 0x53, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x4a, 0x55, 0x4e, 0x49, 0x50, 0x45, 0x52, 0x5f, @@ -1384,7 +1390,8 @@ var file_topo_proto_rawDesc = []byte{ 0x41, 0x5f, 0x54, 0x47, 0x10, 0x0a, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x4f, 0x42, 0x47, 0x50, 0x10, 0x0b, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x49, 0x53, 0x43, 0x4f, 0x5f, 0x58, 0x52, 0x44, 0x10, 0x0c, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x49, 0x53, 0x43, 0x4f, 0x5f, 0x45, 0x38, 0x30, 0x30, 0x30, 0x10, - 0x0d, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x45, 0x4d, 0x4d, 0x49, 0x4e, 0x47, 0x10, 0x0e, 0x4a, 0x04, + 0x0d, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x45, 0x4d, 0x4d, 0x49, 0x4e, 0x47, 0x10, 0x0e, 0x12, 0x0e, + 0x0a, 0x0a, 0x43, 0x49, 0x45, 0x4e, 0x41, 0x5f, 0x53, 0x41, 0x4f, 0x53, 0x10, 0x0f, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x60, 0x0a, 0x0e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x12, 0x40, 0x0a, 0x11, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, @@ -1487,7 +1494,7 @@ var file_topo_proto_rawDesc = []byte{ 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x2a, 0xb4, 0x01, 0x0a, 0x06, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x12, 0x0b, + 0x6d, 0x65, 0x73, 0x2a, 0xbf, 0x01, 0x0a, 0x06, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x52, 0x49, 0x53, 0x54, 0x41, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x49, 0x53, 0x43, 0x4f, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, @@ -1498,10 +1505,11 @@ var file_topo_proto_rawDesc = []byte{ 0x10, 0x09, 0x12, 0x0e, 0x0a, 0x0a, 0x4f, 0x50, 0x45, 0x4e, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x0a, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x4c, 0x50, 0x49, 0x4e, 0x45, 0x10, 0x0b, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x52, 0x49, 0x56, 0x45, 0x4e, 0x45, 0x54, 0x53, 0x10, 0x0c, 0x12, 0x0b, 0x0a, - 0x07, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x10, 0x0d, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x2f, 0x6b, 0x6e, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x6f, - 0x70, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x07, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x10, 0x0d, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x49, + 0x45, 0x4e, 0x41, 0x10, 0x0e, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x6b, + 0x6e, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x6f, 0x70, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/topo/node/ciena/ciena.go b/topo/node/ciena/ciena.go new file mode 100644 index 00000000..550de696 --- /dev/null +++ b/topo/node/ciena/ciena.go @@ -0,0 +1,340 @@ +// Copyright 2025 Ciena Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package ciena + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + + cpb "github.com/openconfig/kne/proto/ciena" + tpb "github.com/openconfig/kne/proto/topo" + "github.com/openconfig/kne/topo/node" + "google.golang.org/protobuf/proto" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + log "k8s.io/klog/v2" + "k8s.io/utils/pointer" +) + +const ( + ModelWR = "waverouter" + ModelSAOS = "5132" // Default SAOS model type +) + +var ( + defaultNode = tpb.Node{ + Name: "default_waverouter_node", + Services: map[uint32]*tpb.Service{ + 22: { + Names: []string{"ssh"}, + Inside: 22, + }, + 179: { + Names: []string{"bgp"}, + Inside: 179, + }, + 225: { + Names: []string{"debug"}, + Inside: 225, + }, + 443: { + Names: []string{"ssl"}, + Inside: 443, + }, + 830: { + Names: []string{"netconf"}, + Inside: 830, + }, + 4243: { + Names: []string{"docker-daemon"}, + Inside: 4243, + }, + 9339: { + Names: []string{"gnmi"}, + Inside: 9339, + }, + 9340: { + Names: []string{"gribi"}, + Inside: 9340, + }, + 9559: { + Names: []string{"p4rt"}, + Inside: 9559, + }, + 10161: { + Names: []string{"gnmi-gnoi-alternate"}, + Inside: 10161, + }, + }, + Model: ModelSAOS, + Os: "saos", + Labels: map[string]string{ + "vendor": tpb.Vendor_CIENA.String(), + node.OndatraRoleLabel: node.OndatraRoleDUT, + }, + Config: &tpb.Config{ + ConfigPath: "config/", + ConfigFile: "config.cfg", + Image: "artifactory.ciena.com/psa/saos-containerlab:latest", + }, + } +) + +func New(nodeImpl *node.Impl) (node.Node, error) { + if nodeImpl == nil { + return nil, fmt.Errorf("nodeImpl cannot be nil") + } + if nodeImpl.Proto == nil { + return nil, fmt.Errorf("nodeImpl.Proto cannot be nil") + } + cfg, err := defaults(nodeImpl.Proto) + if err != nil { + return nil, err + } + nodeImpl.Proto = cfg + n := &Node{ + Impl: nodeImpl, + } + n.Proto.Interfaces = renameInterfaces(nodeImpl.Proto.Interfaces) + return n, nil +} + +// renameInterfaces renames the interfaces port naming to ethX format +// e.g., eth1 -> eth1, 1/5/1 -> eth1, 2 -> eth2, etc +func renameInterfaces(interfaces map[string]*tpb.Interface) map[string]*tpb.Interface { + intf := map[string]*tpb.Interface{} + reFull := regexp.MustCompile(`^\d+/\d+/\d+$`) + reSingle := regexp.MustCompile(`^\d+$`) + for k, v := range interfaces { + switch { + case reFull.MatchString(k): + parts := regexp.MustCompile(`/`).Split(k, -1) + lastNum := parts[len(parts)-1] + name := fmt.Sprintf("eth%s", lastNum) + intf[name] = v + case reSingle.MatchString(k): + name := fmt.Sprintf("eth%s", k) + intf[name] = v + default: + intf[k] = v + } + } + return intf +} + +type Node struct { + *node.Impl +} + +func (n *Node) Create(ctx context.Context) error { + if err := n.ValidateConstraints(); err != nil { + return fmt.Errorf("node %s failed to validate node with errors: %w", n.Name(), err) + } + if err := n.CreatePod(ctx); err != nil { + return fmt.Errorf("node %s failed to create pod %w", n.Name(), err) + } + if err := n.CreateService(ctx); err != nil { + return fmt.Errorf("node %s failed to create service %w", n.Name(), err) + } + return nil +} + +// CreatePod creates a Pod for the Node based on the underlying proto. +func (n *Node) CreatePod(ctx context.Context) error { + pb := n.Proto + log.Infof("Creating Pod:\n %+v", pb) + + var extraVolumes []corev1.Volume + var extraMounts []corev1.VolumeMount + mountPath := "/" + fileName := "setup.json" + + if vendorData := pb.Config.GetVendorData(); vendorData != nil { + cienaConfig := &cpb.CienaConfig{} + + if err := vendorData.UnmarshalTo(cienaConfig); err != nil { + return err + } + + if cienaConfig.GetSystemEquipment().GetMountDir() != "" { + mountPath = cienaConfig.GetSystemEquipment().GetMountDir() + } + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-file", n.Proto.Name), + }, + Data: map[string]string{}, + } + equipmentFile := cienaConfig.GetSystemEquipment().GetEquipmentJson() + + contents, err := os.ReadFile(filepath.Join(n.BasePath, equipmentFile)) + if err != nil { + return err + } + cm.Data[fileName] = string(contents) + extraMounts = append(extraMounts, corev1.VolumeMount{ + Name: "equipment-file", + MountPath: fmt.Sprintf("%s/%s", mountPath, fileName), + SubPath: fileName, + ReadOnly: true, + }) + extraVolumes = append(extraVolumes, corev1.Volume{ + Name: "equipment-file", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-file", n.Proto.Name), + }, + }}, + }) + + _, err = n.KubeClient.CoreV1().ConfigMaps(n.Namespace).Create(ctx, cm, metav1.CreateOptions{}) + if err != nil { + return err + } + } + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: pb.Name, + Labels: map[string]string{ + "app": pb.Name, + "topo": n.Namespace, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: pb.Name, + Image: pb.Config.Image, + Command: pb.Config.Command, + Args: pb.Config.Args, + Env: node.ToEnvVar(pb.Config.Env), + Resources: node.ToResourceRequirements(pb.Constraints), + ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + }, + VolumeMounts: extraMounts, + }}, + Volumes: extraVolumes, + TerminationGracePeriodSeconds: pointer.Int64(0), + NodeSelector: map[string]string{}, + Affinity: &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{{ + Weight: 100, + PodAffinityTerm: corev1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{ + Key: "topo", + Operator: "In", + Values: []string{pb.Name}, + }}, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }}, + }, + }, + }, + } + 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 + } + pod.Spec.Volumes = append(pod.Spec.Volumes, *vol) + vm := corev1.VolumeMount{ + Name: node.ConfigVolumeName, + MountPath: fmt.Sprintf("%s/%s", 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{}) + if err != nil { + return fmt.Errorf("failed to create pod for %q: %w", pb.Name, err) + } + log.V(1).Infof("Pod created:\n%+v\n", sPod) + return nil +} + +func defaults(pb *tpb.Node) (*tpb.Node, error) { + defaultNodeClone := proto.Clone(&defaultNode).(*tpb.Node) + if pb == nil { + pb = &tpb.Node{ + Name: defaultNodeClone.Name, + } + } + if pb.Config == nil { + pb.Config = &tpb.Config{} + } + if pb.Config.ConfigFile == "" { + pb.Config.ConfigFile = defaultNodeClone.Config.ConfigFile + } + if pb.Config.ConfigPath == "" { + pb.Config.ConfigPath = defaultNodeClone.Config.ConfigPath + } + if pb.Model == "" { + pb.Model = defaultNodeClone.Model + } + if pb.Os == "" { + pb.Os = defaultNodeClone.Os + } + if pb.Services == nil { + pb.Services = defaultNodeClone.Services + } + if pb.Labels == nil { + pb.Labels = map[string]string{} + } + if pb.Labels["vendor"] == "" { + pb.Labels["vendor"] = defaultNodeClone.Labels["vendor"] + } + if pb.Labels["model"] == "" { + pb.Labels["model"] = pb.Model + } + if pb.Labels["os"] == "" { + pb.Labels["os"] = pb.Os + } + if pb.Labels[node.OndatraRoleLabel] == "" { + pb.Labels[node.OndatraRoleLabel] = defaultNodeClone.Labels[node.OndatraRoleLabel] + } + if pb.Config.Env == nil { + pb.Config.Env = map[string]string{} + } + pb.Config.Env["CLAB_LABEL_CLAB_NODE_NAME"] = pb.Name + pb.Config.Env["CLAB_LABEL_CLAB_NODE_TYPE"] = pb.Model + + if pb.Model == ModelWR && pb.Config.Image == "" { + pb.Config.Image = "artifactory.ciena.com/psa/rw-containerlab:latest" + } else if pb.Config.Image == "" { + pb.Config.Image = defaultNodeClone.Config.Image + } + return pb, nil +} + +func init() { + node.Vendor(tpb.Vendor_CIENA, New) +} diff --git a/topo/node/ciena/ciena_test.go b/topo/node/ciena/ciena_test.go new file mode 100644 index 00000000..801f8e4e --- /dev/null +++ b/topo/node/ciena/ciena_test.go @@ -0,0 +1,396 @@ +// Copyright 2025 Ciena Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package ciena + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + cpb "github.com/openconfig/kne/proto/ciena" + tpb "github.com/openconfig/kne/proto/topo" + "github.com/openconfig/kne/topo/node" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kfake "k8s.io/client-go/kubernetes/fake" +) + +func TestNew(t *testing.T) { + tests := []struct { + desc string + nImpl *node.Impl + want *tpb.Node + wantErr string + }{ + { + desc: "nil nodeImpl", + nImpl: nil, + wantErr: "nodeImpl cannot be nil", + }, + { + desc: "nil nodeImpl.Proto", + nImpl: &node.Impl{}, + wantErr: "nodeImpl.Proto cannot be nil", + }, + { + desc: "empty proto, expect defaults", + nImpl: &node.Impl{ + Proto: &tpb.Node{}, + }, + want: &tpb.Node{ + // Name: "default_waverouter_node", + Model: "5132", + Os: "saos", + Labels: map[string]string{ + "vendor": tpb.Vendor_CIENA.String(), + "ondatra-role": "DUT", + "model": "5132", + "os": "saos", + }, + Config: &tpb.Config{ + ConfigPath: "config/", + ConfigFile: "config.cfg", + Image: "artifactory.ciena.com/psa/saos-containerlab:latest", + Env: map[string]string{ + "CLAB_LABEL_CLAB_NODE_NAME": "", + "CLAB_LABEL_CLAB_NODE_TYPE": "5132", + }, + }, + Services: map[uint32]*tpb.Service{ + 22: {Names: []string{"ssh"}, Inside: 22}, + 179: {Names: []string{"bgp"}, Inside: 179}, + 225: {Names: []string{"debug"}, Inside: 225}, + 443: {Names: []string{"ssl"}, Inside: 443}, + 830: {Names: []string{"netconf"}, Inside: 830}, + 4243: {Names: []string{"docker-daemon"}, Inside: 4243}, + 9339: {Names: []string{"gnmi"}, Inside: 9339}, + 9340: {Names: []string{"gribi"}, Inside: 9340}, + 9559: {Names: []string{"p4rt"}, Inside: 9559}, + 10161: {Names: []string{"gnmi-gnoi-alternate"}, Inside: 10161}, + }, + Interfaces: map[string]*tpb.Interface{}, + }, + }, + { + desc: "proto with interfaces, expect rename", + nImpl: &node.Impl{ + Proto: &tpb.Node{ + Interfaces: map[string]*tpb.Interface{ + "1/2/3": {IntName: "1/2/3"}, + "5": {IntName: "5"}, + "eth7": {IntName: "eth7"}, + "mgmt": {IntName: "mgmt"}, + }, + }, + }, + want: &tpb.Node{ + // Name: "default_waverouter_node", + Model: "5132", + Os: "saos", + Labels: map[string]string{ + "vendor": tpb.Vendor_CIENA.String(), + "ondatra-role": "DUT", + "model": "5132", + "os": "saos", + }, + Config: &tpb.Config{ + ConfigPath: "config/", + ConfigFile: "config.cfg", + Image: "artifactory.ciena.com/psa/saos-containerlab:latest", + Env: map[string]string{ + "CLAB_LABEL_CLAB_NODE_NAME": "", + "CLAB_LABEL_CLAB_NODE_TYPE": "5132", + }, + }, + Services: map[uint32]*tpb.Service{ + 22: {Names: []string{"ssh"}, Inside: 22}, + 179: {Names: []string{"bgp"}, Inside: 179}, + 225: {Names: []string{"debug"}, Inside: 225}, + 443: {Names: []string{"ssl"}, Inside: 443}, + 830: {Names: []string{"netconf"}, Inside: 830}, + 4243: {Names: []string{"docker-daemon"}, Inside: 4243}, + 9339: {Names: []string{"gnmi"}, Inside: 9339}, + 9340: {Names: []string{"gribi"}, Inside: 9340}, + 9559: {Names: []string{"p4rt"}, Inside: 9559}, + 10161: {Names: []string{"gnmi-gnoi-alternate"}, Inside: 10161}, + }, + Interfaces: map[string]*tpb.Interface{ + "eth3": {IntName: "1/2/3"}, + "eth5": {IntName: "5"}, + "eth7": {IntName: "eth7"}, + "mgmt": {IntName: "mgmt"}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + n, err := New(tt.nImpl) + if tt.wantErr != "" { + if err == nil || err.Error() != tt.wantErr { + t.Fatalf("got err %v, want %v", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + got := n.GetProto() + if !proto.Equal(got, tt.want) { + t.Errorf("proto mismatch (-want +got):\nwant: %v\ngot: %v", tt.want, got) + } + }) + } +} +func TestRenameInterfaces(t *testing.T) { + tests := []struct { + desc string + input map[string]*tpb.Interface + want map[string]*tpb.Interface + }{ + { + desc: "empty interfaces", + input: map[string]*tpb.Interface{}, + want: map[string]*tpb.Interface{}, + }, + { + desc: "single full format", + input: map[string]*tpb.Interface{ + "1/5/2": {IntName: "1/5/2"}, + }, + want: map[string]*tpb.Interface{ + "eth2": {IntName: "1/5/2"}, + }, + }, + { + desc: "single digit format", + input: map[string]*tpb.Interface{ + "3": {IntName: "3"}, + }, + want: map[string]*tpb.Interface{ + "eth3": {IntName: "3"}, + }, + }, + { + desc: "already eth format", + input: map[string]*tpb.Interface{ + "eth1": {IntName: "eth1"}, + }, + want: map[string]*tpb.Interface{ + "eth1": {IntName: "eth1"}, + }, + }, + { + desc: "mgmt interface", + input: map[string]*tpb.Interface{ + "mgmt": {IntName: "mgmt"}, + }, + want: map[string]*tpb.Interface{ + "mgmt": {IntName: "mgmt"}, + }, + }, + { + desc: "multiple mixed formats", + input: map[string]*tpb.Interface{ + "1/2/3": {IntName: "1/2/3"}, + "5": {IntName: "5"}, + "eth7": {IntName: "eth7"}, + "mgmt": {IntName: "mgmt"}, + }, + want: map[string]*tpb.Interface{ + "eth3": {IntName: "1/2/3"}, + "eth5": {IntName: "5"}, + "eth7": {IntName: "eth7"}, + "mgmt": {IntName: "mgmt"}, + }, + }, + { + desc: "non-matching string", + input: map[string]*tpb.Interface{ + "foo": {IntName: "foo"}, + }, + want: map[string]*tpb.Interface{ + "foo": {IntName: "foo"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + got := renameInterfaces(tt.input) + if diff := cmp.Diff(tt.want, got, cmp.Comparer(proto.Equal)); diff != "" { + t.Errorf("renameInterfaces() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestNode_CreatePod_EquipmentFile(t *testing.T) { + tmpDir := t.TempDir() + equipmentFile := "equipment.json" + equipmentContent := `{"equipment": "test"}` + equipmentPath := filepath.Join(tmpDir, equipmentFile) + if err := os.WriteFile(equipmentPath, []byte(equipmentContent), 0644); err != nil { + t.Fatalf("failed to write equipment file: %v", err) + } + + vendorData, err := anypb.New(&cpb.CienaConfig{ + SystemEquipment: &cpb.SystemEquipment{ + MountDir: "/equipment", + EquipmentJson: equipmentFile, + }, + }) + if err != nil { + t.Fatalf("failed to marshal vendor data: %v", err) + } + + nodeProto := &tpb.Node{ + Name: "wr-1", + Config: &tpb.Config{ + Image: "vrnetlab/ciena_waverouter:config", + VendorData: vendorData, + }, + } + + n := &Node{ + Impl: &node.Impl{ + Namespace: "testns", + KubeClient: kfake.NewSimpleClientset(), + Proto: nodeProto, + BasePath: tmpDir, + }, + } + + err = n.CreatePod(context.Background()) + if err != nil { + t.Fatalf("CreatePod() failed: %v", err) + } + + pod, err := n.KubeClient.CoreV1().Pods(n.Namespace).Get(context.Background(), n.Name(), metav1.GetOptions{}) + if err != nil { + t.Fatalf("Could not get the pod: %v", err) + } + + // Check container + if len(pod.Spec.Containers) != 1 { + t.Fatalf("expected 1 container, got %d", len(pod.Spec.Containers)) + } + ctr := pod.Spec.Containers[0] + if ctr.Name != "wr-1" { + t.Errorf("container name: got %q, want %q", ctr.Name, "wr-1") + } + if ctr.Image != "vrnetlab/ciena_waverouter:config" { + t.Errorf("container image: got %q, want %q", ctr.Image, "vrnetlab/ciena_waverouter:config") + } + foundMount := false + for _, m := range ctr.VolumeMounts { + if m.Name == "equipment-file" && m.MountPath == "/equipment/setup.json" && m.SubPath == "setup.json" && m.ReadOnly { + foundMount = true + } + } + if !foundMount { + t.Errorf("expected equipment-file mount not found in container: %+v", ctr.VolumeMounts) + } + + // Check volumes + foundVol := false + for _, v := range pod.Spec.Volumes { + if v.Name == "equipment-file" && v.VolumeSource.ConfigMap != nil && v.VolumeSource.ConfigMap.Name == "wr-1-file" { + foundVol = true + } + } + if !foundVol { + t.Errorf("expected equipment-file volume not found in pod: %+v", pod.Spec.Volumes) + } +} + +func TestNode_CreatePod_MissingEquipmentFile(t *testing.T) { + tmpDir := t.TempDir() + vendorData, err := anypb.New(&cpb.CienaConfig{ + SystemEquipment: &cpb.SystemEquipment{ + MountDir: "/equipment", + EquipmentJson: "nonexistent.json", + }, + }) + if err != nil { + t.Fatalf("failed to marshal vendor data: %v", err) + } + + nodeProto := &tpb.Node{ + Name: "wr-2", + Config: &tpb.Config{ + Image: "vrnetlab/ciena_waverouter:config", + VendorData: vendorData, + }, + } + + n := &Node{ + Impl: &node.Impl{ + Namespace: "testns", + KubeClient: kfake.NewSimpleClientset(), + Proto: nodeProto, + BasePath: tmpDir, + }, + } + + err = n.CreatePod(context.Background()) + if err == nil { + t.Fatalf("expected error due to missing equipment file, got nil") + } +} + +func TestNode_CreatePod_NoVendorData(t *testing.T) { + nodeProto := &tpb.Node{ + Name: "wr-3", + Config: &tpb.Config{ + Image: "vrnetlab/ciena_waverouter:config", + }, + } + + n := &Node{ + Impl: &node.Impl{ + Namespace: "testns", + KubeClient: kfake.NewSimpleClientset(), + Proto: nodeProto, + }, + } + + err := n.CreatePod(context.Background()) + if err != nil { + t.Fatalf("CreatePod() failed: %v", err) + } + + pod, err := n.KubeClient.CoreV1().Pods(n.Namespace).Get(context.Background(), n.Name(), metav1.GetOptions{}) + if err != nil { + t.Fatalf("Could not get the pod: %v", err) + } + + if len(pod.Spec.Containers) != 1 { + t.Fatalf("expected 1 container, got %d", len(pod.Spec.Containers)) + } + ctr := pod.Spec.Containers[0] + if ctr.Name != "wr-3" { + t.Errorf("container name: got %q, want %q", ctr.Name, "wr-3") + } + if ctr.Image != "vrnetlab/ciena_waverouter:config" { + t.Errorf("container image: got %q, want %q", ctr.Image, "vrnetlab/ciena_waverouter:config") + } + if len(ctr.VolumeMounts) != 0 { + t.Errorf("expected no volume mounts, got %+v", ctr.VolumeMounts) + } +} \ No newline at end of file diff --git a/topo/topo.go b/topo/topo.go index 85d5e2c7..31e9702c 100644 --- a/topo/topo.go +++ b/topo/topo.go @@ -62,6 +62,7 @@ import ( _ "github.com/openconfig/kne/topo/node/keysight" _ "github.com/openconfig/kne/topo/node/nokia" _ "github.com/openconfig/kne/topo/node/openconfig" + _ "github.com/openconfig/kne/topo/node/ciena" ) var (