diff --git a/controller/api/initialise_test.go b/controller/api/initialise_test.go index c9363a7f44540d527caa8c04cfcc441645307752..ab531d3d8232d26653292413ffab973450a70921 100644 --- a/controller/api/initialise_test.go +++ b/controller/api/initialise_test.go @@ -43,7 +43,7 @@ const sbiID = "f6fd4b35-f039-4111-9156-5e4501bb8a5a" const ondID = "7e0ed8cc-ebf5-46fa-9794-741494914883" var pndStore networkdomain.PndStore -var sbiStore southbound.SbiStore +var sbiStore southbound.Store var lis *bufconn.Listener var pndUUID uuid.UUID var sbiUUID uuid.UUID diff --git a/controller/interfaces/device/device.go b/controller/interfaces/device/device.go index 55481cad37415c98335fe23ade9d29e70a0a3767..84a5dce80863728e8b53525656cd4965dbdc1ee3 100644 --- a/controller/interfaces/device/device.go +++ b/controller/interfaces/device/device.go @@ -27,3 +27,25 @@ type Details struct { Address string TransportOption *tpb.TransportOption } + +// LoadedDevice represents a Orchestrated Networking Device that was loaeded +// by using the Load() method of the DeviceStore. +type LoadedDevice struct { + // DeviceID represents the UUID of the LoadedDevice. + DeviceID string `json:"id" bson:"_id"` + // Name represents the name of the LoadedDevice. + Name string `json:"name,omitempty"` + // TransportType represent the type of the transport in use of the LoadedDevice. + TransportType string `json:"transport_type,omitempty" bson:"transport_type,omitempty"` + // TransportAddress represents the address from which the device can be reached via the transport method. + TransportAddress string `json:"transport_address,omitempty" bson:"transport_address,omitempty"` + // TransportUsername is used for authentication via the transport method in use. + TransportUsername string `json:"transport_username,omitempty" bson:"transport_username,omitempty"` + // TransportPassword is used for authentication via the transport method in use. + TransportPassword string `json:"transport_password,omitempty" bson:"transport_password,omitempty"` + TransportOptionCsbi bool `json:"transport_option_csbi,omitempty" bson:"transport_option_csbi,omitempty"` + + // SBI indicates the southbound interface, which is used by this device as UUID. + SBI string `json:"sbi"` + Model string `json:"model,omitempty" bson:"model,omitempty"` +} diff --git a/controller/interfaces/device/deviceService.go b/controller/interfaces/device/deviceService.go new file mode 100644 index 0000000000000000000000000000000000000000..23417157302cedcb024cc94d68bbefb37e271d15 --- /dev/null +++ b/controller/interfaces/device/deviceService.go @@ -0,0 +1,14 @@ +package device + +import ( + "code.fbi.h-da.de/danet/gosdn/controller/store" +) + +// Service describes an interface for device service implementations. +type Service interface { + Add(Device) error + Update(Device) error + Delete(Device) error + Get(store.Query) (Device, error) + GetAll() ([]Device, error) +} diff --git a/controller/interfaces/device/deviceStore.go b/controller/interfaces/device/deviceStore.go index 10a056771883b3bd4886050ab25f82c099829c12..6fba480e52fc2f6f0af030c6f69b0cf42c8804bb 100644 --- a/controller/interfaces/device/deviceStore.go +++ b/controller/interfaces/device/deviceStore.go @@ -6,9 +6,9 @@ import ( // Store describes an interface for device store implementations. type Store interface { - Add(d Device) error - Update(d Device) error + Add(Device) error + Update(Device) error Delete(Device) error - Get(store.Query) (Device, error) - GetAll() ([]Device, error) + Get(store.Query) (LoadedDevice, error) + GetAll() ([]LoadedDevice, error) } diff --git a/controller/interfaces/southbound/Service.go b/controller/interfaces/southbound/Service.go new file mode 100644 index 0000000000000000000000000000000000000000..763bd7a46f8568cffd9a10fff92c3f95c5a5cece --- /dev/null +++ b/controller/interfaces/southbound/Service.go @@ -0,0 +1,13 @@ +package southbound + +import ( + "code.fbi.h-da.de/danet/gosdn/controller/store" +) + +// Service describes an interface for sbi service implementations. +type Service interface { + Add(SouthboundInterface) error + Delete(SouthboundInterface) error + Get(store.Query) (SouthboundInterface, error) + GetAll() ([]SouthboundInterface, error) +} diff --git a/controller/interfaces/southbound/sbi.go b/controller/interfaces/southbound/sbi.go index 485a54ced2951a2b66ec01ec56ee3839f9df6e04..ffad0638adc2923438f44aa8ee9e6bc9d78dd43a 100644 --- a/controller/interfaces/southbound/sbi.go +++ b/controller/interfaces/southbound/sbi.go @@ -25,3 +25,11 @@ type SouthboundInterface interface { // nolint Unmarshal([]byte, *gpb.Path, ygot.ValidatedGoStruct, ...ytypes.UnmarshalOpt) error Name() string } + +// LoadedSbi represents a Southbound Interface that was loaded by using +// the Load() method of the SbiStore. +type LoadedSbi struct { + ID string `json:"_id" bson:"_id"` + Type spb.Type `bson:"type"` + Path string `json:"path,omitempty"` +} diff --git a/controller/interfaces/southbound/sbiStore.go b/controller/interfaces/southbound/sbiStore.go index bf69ec54b335336f66782a6f8fb70f8e8f549933..8c33372cefd13764b1bb23bb71275ffe2fbba943 100644 --- a/controller/interfaces/southbound/sbiStore.go +++ b/controller/interfaces/southbound/sbiStore.go @@ -4,10 +4,10 @@ import ( "code.fbi.h-da.de/danet/gosdn/controller/store" ) -// SbiStore describes an interface for sbi store implementations. -type SbiStore interface { +// Store describes an interface for sbi store implementations. +type Store interface { Add(SouthboundInterface) error Delete(SouthboundInterface) error - Get(store.Query) (SouthboundInterface, error) - GetAll() ([]SouthboundInterface, error) + Get(store.Query) (LoadedSbi, error) + GetAll() ([]LoadedSbi, error) } diff --git a/controller/northbound/server/pnd_test.go b/controller/northbound/server/pnd_test.go index f95be5acc5af3975b87e3dd3d8151a57c5181466..b2acf2d7f3ddcbaa68d7f3dc2b9743850adb45d7 100644 --- a/controller/northbound/server/pnd_test.go +++ b/controller/northbound/server/pnd_test.go @@ -44,7 +44,7 @@ var committedChangeUUID uuid.UUID var deviceUUID uuid.UUID var mockPnd *mocks.NetworkDomain var mockDevice device.Device -var sbiStore southbound.SbiStore +var sbiStore southbound.Store func callback(id uuid.UUID, ch chan device.Details) { // Need for pnd creation, but not needed for this test case. diff --git a/controller/nucleus/databaseDeviceStore.go b/controller/nucleus/databaseDeviceStore.go index fecf9f09ee1ba433518a77d2628dddf8aaa2115c..5ebf57278af54b36dca3cecc4db6f177fabf6d8d 100644 --- a/controller/nucleus/databaseDeviceStore.go +++ b/controller/nucleus/databaseDeviceStore.go @@ -3,10 +3,7 @@ package nucleus import ( "fmt" - spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound" - tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport" "code.fbi.h-da.de/danet/gosdn/controller/interfaces/device" - "code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound" "code.fbi.h-da.de/danet/gosdn/controller/nucleus/database" "code.fbi.h-da.de/danet/gosdn/controller/nucleus/errors" "code.fbi.h-da.de/danet/gosdn/controller/store" @@ -15,58 +12,44 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" "github.com/google/uuid" - "github.com/openconfig/ygot/ygot" log "github.com/sirupsen/logrus" ) // DatabaseDeviceStore is used to store Devices type DatabaseDeviceStore struct { storeName string - sbiStore southbound.SbiStore } // NewDatabaseDeviceStore returns a DeviceStore -func NewDatabaseDeviceStore(pndUUID uuid.UUID, sbiStore southbound.SbiStore) device.Store { +func NewDatabaseDeviceStore(pndUUID uuid.UUID) device.Store { return &DatabaseDeviceStore{ storeName: fmt.Sprintf("device-store-%s.json", pndUUID.String()), - sbiStore: sbiStore, } } // Get takes a Device's UUID or name and returns the Device. -func (s *DatabaseDeviceStore) Get(query store.Query) (device.Device, error) { - var loadedDevice LoadedDevice - var err error +func (s *DatabaseDeviceStore) Get(query store.Query) (device.LoadedDevice, error) { + var loadedDevice device.LoadedDevice if query.ID.String() != "" { - loadedDevice, err = s.getByID(query.ID) + loadedDevice, err := s.getByID(query.ID) if err != nil { - return nil, errors.ErrCouldNotFind{StoreName: deviceStoreName} + return loadedDevice, errors.ErrCouldNotFind{StoreName: deviceStoreName} } - device, err := s.createDeviceFromStore(loadedDevice) - if err != nil { - return nil, err - } - - return device, nil + return loadedDevice, nil } - loadedDevice, err = s.getByName(query.Name) + loadedDevice, err := s.getByName(query.Name) if err != nil { - return nil, errors.ErrCouldNotFind{StoreName: deviceStoreName} - } - - device, err := s.createDeviceFromStore(loadedDevice) - if err != nil { - return nil, err + return loadedDevice, errors.ErrCouldNotFind{StoreName: deviceStoreName} } - return device, nil + return loadedDevice, nil } -func (s *DatabaseDeviceStore) getByID(idOfDevice uuid.UUID) (LoadedDevice, error) { - var loadedDevice LoadedDevice +func (s *DatabaseDeviceStore) getByID(idOfDevice uuid.UUID) (device.LoadedDevice, error) { + var loadedDevice device.LoadedDevice client, ctx, cancel := database.GetMongoConnection() defer cancel() @@ -88,8 +71,8 @@ func (s *DatabaseDeviceStore) getByID(idOfDevice uuid.UUID) (LoadedDevice, error return loadedDevice, nil } -func (s *DatabaseDeviceStore) getByName(nameOfDevice string) (LoadedDevice, error) { - var loadedDevice LoadedDevice +func (s *DatabaseDeviceStore) getByName(nameOfDevice string) (device.LoadedDevice, error) { + var loadedDevice device.LoadedDevice client, ctx, cancel := database.GetMongoConnection() defer cancel() @@ -112,9 +95,8 @@ func (s *DatabaseDeviceStore) getByName(nameOfDevice string) (LoadedDevice, erro } // GetAll returns all stored devices. -func (s *DatabaseDeviceStore) GetAll() ([]device.Device, error) { - var loadedDevices []LoadedDevice - var devices []device.Device +func (s *DatabaseDeviceStore) GetAll() ([]device.LoadedDevice, error) { + var loadedDevices []device.LoadedDevice client, ctx, cancel := database.GetMongoConnection() defer cancel() @@ -135,16 +117,7 @@ func (s *DatabaseDeviceStore) GetAll() ([]device.Device, error) { return nil, errors.ErrCouldNotMarshall{StoreName: pndStoreName} } - for _, loadedDevice := range loadedDevices { - device, err := s.createDeviceFromStore(loadedDevice) - if err != nil { - return nil, err - } - - devices = append(devices, device) - } - - return devices, nil + return loadedDevices, nil } // Add adds a device to the device store. @@ -166,7 +139,7 @@ func (s *DatabaseDeviceStore) Add(deviceToAdd device.Device) error { // Update updates a existing device. func (s *DatabaseDeviceStore) Update(deviceToUpdate device.Device) error { - var updatedLoadedDevice LoadedDevice + var updatedLoadedDevice device.LoadedDevice client, ctx, cancel := database.GetMongoConnection() defer cancel() @@ -210,41 +183,3 @@ func (s *DatabaseDeviceStore) Delete(deviceToDelete device.Device) error { return nil } - -func (s *DatabaseDeviceStore) createDeviceFromStore(loadedDevice LoadedDevice) (device.Device, error) { - sbiForDevice, err := s.sbiStore.Get(store.Query{ID: uuid.MustParse(loadedDevice.SBI)}) - if err != nil { - return nil, err - } - - d, err := NewDevice( - loadedDevice.Name, - uuid.MustParse(loadedDevice.DeviceID), - &tpb.TransportOption{ - Address: loadedDevice.TransportAddress, - Username: loadedDevice.TransportUsername, - Password: loadedDevice.TransportPassword, - TransportOption: &tpb.TransportOption_GnmiTransportOption{ - GnmiTransportOption: &tpb.GnmiTransportOption{}, - }, - Type: spb.Type_TYPE_OPENCONFIG, - }, sbiForDevice) - - if err != nil { - return nil, err - } - - // Create 'root' path to be able to load the whole model from the store. - path, err := ygot.StringToPath("/", ygot.StructuredPath) - if err != nil { - return nil, err - } - - // Use unmarshall from the devices SBI to unmarshall ygot json in go struct. - err = d.SBI().Unmarshal([]byte(loadedDevice.Model), path, d.GetModel()) - if err != nil { - return nil, err - } - - return d, nil -} diff --git a/controller/nucleus/databaseSbiStore.go b/controller/nucleus/databaseSbiStore.go index abaaebc4f59ee3aaca95b17ebd6a27f4ed78b1d8..4c1ebe70f8226538a578e1a9165f02a50b4384c2 100644 --- a/controller/nucleus/databaseSbiStore.go +++ b/controller/nucleus/databaseSbiStore.go @@ -49,7 +49,7 @@ func (s *DatabaseSbiStore) Delete(item southbound.SouthboundInterface) error { Collection(s.sbiStoreName). DeleteOne(ctx, bson.D{primitive.E{Key: "_id", Value: item.ID().String()}}) if err != nil { - return errors.ErrCouldNotCreate{StoreName: sbiStoreName} + return errors.ErrCouldNotDelete{StoreName: sbiStoreName} } return nil @@ -57,8 +57,8 @@ func (s *DatabaseSbiStore) Delete(item southbound.SouthboundInterface) error { // Get takes a SouthboundInterface's UUID or name and returns the SouthboundInterface. If the requested // SouthboundInterface does not exist an error is returned. -func (s *DatabaseSbiStore) Get(query store.Query) (southbound.SouthboundInterface, error) { - var loadedSbi *LoadedSbi +func (s *DatabaseSbiStore) Get(query store.Query) (southbound.LoadedSbi, error) { + var loadedSbi southbound.LoadedSbi client, ctx, cancel := database.GetMongoConnection() defer cancel() @@ -70,25 +70,22 @@ func (s *DatabaseSbiStore) Get(query store.Query) (southbound.SouthboundInterfac collection := db.Collection(s.sbiStoreName) result := collection.FindOne(ctx, bson.D{primitive.E{Key: "_id", Value: query.ID.String()}}) if result == nil { - return nil, nil + return loadedSbi, nil } err := result.Decode(&loadedSbi) if err != nil { log.Printf("Failed marshalling %v", err) - return nil, errors.ErrCouldNotMarshall{StoreName: sbiStoreName} + return loadedSbi, errors.ErrCouldNotMarshall{StoreName: sbiStoreName} } - newSbi, _ := NewSBI(loadedSbi.Type, uuid.MustParse(loadedSbi.ID)) - - return newSbi, nil + return loadedSbi, nil } // GetAll returns all SBIs -func (s *DatabaseSbiStore) GetAll() ([]southbound.SouthboundInterface, error) { - var loadedSbis []LoadedSbi - var sbis []southbound.SouthboundInterface +func (s *DatabaseSbiStore) GetAll() ([]southbound.LoadedSbi, error) { + var loadedSbis []southbound.LoadedSbi client, ctx, cancel := database.GetMongoConnection() defer cancel() @@ -109,18 +106,13 @@ func (s *DatabaseSbiStore) GetAll() ([]southbound.SouthboundInterface, error) { return nil, errors.ErrCouldNotMarshall{StoreName: sbiStoreName} } - for _, sbi := range loadedSbis { - newSbi, _ := NewSBI(sbi.Type, uuid.MustParse(sbi.ID)) - sbis = append(sbis, newSbi) - } - - return sbis, nil + return loadedSbis, nil } // GetSBI takes a SouthboundInterface's UUID or name and returns the SouthboundInterface. If the requested // SouthboundInterface does not exist an error is returned. -func (s *DatabaseSbiStore) GetSBI(id uuid.UUID) (southbound.SouthboundInterface, error) { - var loadedSbi LoadedSbi +func (s *DatabaseSbiStore) GetSBI(id uuid.UUID) (southbound.LoadedSbi, error) { + var loadedSbi southbound.LoadedSbi client, ctx, cancel := database.GetMongoConnection() defer cancel() @@ -130,19 +122,17 @@ func (s *DatabaseSbiStore) GetSBI(id uuid.UUID) (southbound.SouthboundInterface, collection := db.Collection(s.sbiStoreName) result := collection.FindOne(ctx, bson.D{primitive.E{Key: "_id", Value: id.String()}}) if result == nil { - return nil, nil + return loadedSbi, nil } err := result.Decode(&loadedSbi) if err != nil { log.Printf("Failed marshalling %v", err) - return nil, errors.ErrCouldNotMarshall{StoreName: sbiStoreName} + return loadedSbi, errors.ErrCouldNotMarshall{StoreName: sbiStoreName} } - newSbi, err := NewSBI(loadedSbi.Type, uuid.MustParse(loadedSbi.ID)) - - return newSbi, nil + return loadedSbi, nil } func getSbiTypeFromString(sbiTypeAsString string) spb.Type { diff --git a/controller/nucleus/deviceService.go b/controller/nucleus/deviceService.go new file mode 100644 index 0000000000000000000000000000000000000000..6679b974e63cbd4c9cc7360271da2152a2399830 --- /dev/null +++ b/controller/nucleus/deviceService.go @@ -0,0 +1,136 @@ +package nucleus + +import ( + "fmt" + + spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound" + "code.fbi.h-da.de/danet/gosdn/controller/interfaces/device" + "code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound" + "code.fbi.h-da.de/danet/gosdn/controller/store" + "github.com/google/uuid" + "github.com/openconfig/ygot/ygot" + + tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport" +) + +// DeviceService provides a device service implementation. +// This services provides abstraction between the user (e.g a PND) and the matching store (e.g. deviceStore) +type DeviceService struct { + deviceStore device.Store + sbiService southbound.Service +} + +// NewDeviceService creates a device service. +func NewDeviceService(deviceStore device.Store, sbiService southbound.Service) device.Service { + return &DeviceService{ + deviceStore: deviceStore, + sbiService: sbiService, + } +} + +// Get takes a Device's UUID or name and returns the Device. +func (s *DeviceService) Get(query store.Query) (device.Device, error) { + loadedDevice, err := s.deviceStore.Get(query) + if err != nil { + return nil, err + } + + device, err := s.createDeviceFromStore(loadedDevice) + if err != nil { + return nil, err + } + + return device, nil +} + +// GetAll returns all stored devices. +func (s *DeviceService) GetAll() ([]device.Device, error) { + var devices []device.Device + + loadedDevices, err := s.deviceStore.GetAll() + if err != nil { + return nil, err + } + + for _, loadedDevice := range loadedDevices { + device, err := s.createDeviceFromStore(loadedDevice) + if err != nil { + return nil, err + } + + devices = append(devices, device) + } + + return devices, nil +} + +// Add adds a device to the device store. +func (s *DeviceService) Add(deviceToAdd device.Device) error { + err := s.deviceStore.Add(deviceToAdd) + if err != nil { + return err + } + + return nil +} + +// Update updates a existing device. +func (s *DeviceService) Update(deviceToUpdate device.Device) error { + err := s.deviceStore.Update(deviceToUpdate) + if err != nil { + return err + } + + return nil +} + +// Delete deletes a device from the device store. +func (s *DeviceService) Delete(deviceToDelete device.Device) error { + err := s.deviceStore.Delete(deviceToDelete) + if err != nil { + return err + } + + return nil +} + +func (s *DeviceService) createDeviceFromStore(loadedDevice device.LoadedDevice) (device.Device, error) { + if loadedDevice.SBI == "" { + return nil, fmt.Errorf("no sbi found for device") + } + + sbiForDevice, err := s.sbiService.Get(store.Query{ID: uuid.MustParse(loadedDevice.SBI)}) + if err != nil { + return nil, err + } + + d, err := NewDevice( + loadedDevice.Name, + uuid.MustParse(loadedDevice.DeviceID), + &tpb.TransportOption{ + Address: loadedDevice.TransportAddress, + Username: loadedDevice.TransportUsername, + Password: loadedDevice.TransportPassword, + TransportOption: &tpb.TransportOption_GnmiTransportOption{ + GnmiTransportOption: &tpb.GnmiTransportOption{}, + }, + Type: spb.Type_TYPE_OPENCONFIG, + }, sbiForDevice) + if err != nil { + return nil, err + } + + // Create 'root' path to be able to load the whole model from the store. + path, err := ygot.StringToPath("/", ygot.StructuredPath) + if err != nil { + return nil, err + } + + // Use unmarshall from the devices SBI to unmarshall ygot json in go struct. + err = d.SBI().Unmarshal([]byte(loadedDevice.Model), path, d.GetModel()) + if err != nil { + return nil, err + } + + return d, nil +} diff --git a/controller/nucleus/deviceService_test.go b/controller/nucleus/deviceService_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b4ee228dd0d2475a84f47077fb9ad6627a95e5b5 --- /dev/null +++ b/controller/nucleus/deviceService_test.go @@ -0,0 +1,109 @@ +package nucleus + +import ( + "testing" + + "code.fbi.h-da.de/danet/gosdn/controller/interfaces/device" + "code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound" + "code.fbi.h-da.de/danet/gosdn/controller/mocks" + "code.fbi.h-da.de/danet/gosdn/controller/store" + "github.com/google/uuid" + + spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound" +) + +func getMockDevice(deviceID uuid.UUID, sbi southbound.SouthboundInterface) device.Device { + return &CommonDevice{ + UUID: deviceID, + Model: sbi.Schema().Root, + sbi: sbi, + transport: &mocks.Transport{}, + } +} + +func getDeviceTestStores(t *testing.T, deviceID uuid.UUID) (device.Service, southbound.Service, device.Device, southbound.SouthboundInterface) { + sbiStore := NewMemorySbiStore() + deviceStore := NewMemoryDeviceStore() + sbiService := NewSbiService(sbiStore) + deviceService := NewDeviceService( + deviceStore, + sbiService, + ) + + sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) + if err != nil { + t.Error("could not create sbi") + } + + err = sbiService.Add(sbi) + if err != nil { + t.Error("could not add sbi") + } + + mockDevice := getMockDevice(deviceID, sbi) + err = deviceService.Add(mockDevice) + if err != nil { + t.Error("could not add device") + } + + return deviceService, sbiService, mockDevice, sbi +} + +func TestDeviceService_Get(t *testing.T) { + deviceID := uuid.New() + + deviceService, _, mockDevice, _ := getDeviceTestStores(t, deviceID) + + device, err := deviceService.Get(store.Query{ + ID: mockDevice.ID(), + Name: mockDevice.Name(), + }) + if err != nil { + t.Error("could not get device") + } + + if mockDevice.ID() != device.ID() { + t.Errorf("Expected ID=%s, got %s", mockDevice.ID(), device.ID()) + } +} + +func TestDeviceService_Delete(t *testing.T) { + deviceID := uuid.New() + + deviceService, _, mockDevice, _ := getDeviceTestStores(t, deviceID) + + device, err := deviceService.Get(store.Query{ + ID: mockDevice.ID(), + Name: mockDevice.Name(), + }) + if err != nil { + t.Error("could not get device") + } + + err = deviceService.Delete(device) + if err != nil { + t.Error("could not delete device") + } +} + +func TestDeviceService_GetAll(t *testing.T) { + deviceID := uuid.New() + deviceID2 := uuid.New() + + deviceService, _, _, sbi := getDeviceTestStores(t, deviceID) + mockDevice2 := getMockDevice(deviceID2, sbi) + + err := deviceService.Add(mockDevice2) + if err != nil { + t.Error("could not add device") + } + + devices, err := deviceService.GetAll() + if err != nil { + t.Error("could not get all devices") + } + + if len(devices) != 2 { + t.Errorf("Expected len(devices)=2, got %d", len(devices)) + } +} diff --git a/controller/nucleus/deviceStore.go b/controller/nucleus/deviceStore.go index d47f4527e0b9e1e88800a8f3127430a651fc0665..04fa24ba7b03bd53f19292c272d464bf2d3cf3cd 100644 --- a/controller/nucleus/deviceStore.go +++ b/controller/nucleus/deviceStore.go @@ -18,55 +18,27 @@ const ( // DeviceStore is used to store Devices type DeviceStore struct { storeName string - sbiStore southbound.SbiStore -} - -// LoadedDevice represents a Orchestrated Networking Device that was loaeded -// by using the Load() method of the DeviceStore. -type LoadedDevice struct { - // DeviceID represents the UUID of the LoadedDevice. - DeviceID string `json:"id" bson:"_id,omitempty"` - // Name represents the name of the LoadedDevice. - Name string `json:"name,omitempty"` - // TransportType represent the type of the transport in use of the LoadedDevice. - TransportType string `json:"transport_type,omitempty" bson:"transport_type,omitempty"` - // TransportAddress represents the address from which the device can be reached via the transport method. - TransportAddress string `json:"transport_address,omitempty" bson:"transport_address,omitempty"` - // TransportUsername is used for authentication via the transport method in use. - TransportUsername string `json:"transport_username,omitempty" bson:"transport_username,omitempty"` - // TransportPassword is used for authentication via the transport method in use. - TransportPassword string `json:"transport_password,omitempty" bson:"transport_password,omitempty"` - TransportOptionCsbi bool `json:"transport_option_csbi,omitempty" bson:"transport_option_csbi,omitempty"` - - // SBI indicates the southbound interface, which is used by this device as UUID. - SBI string `json:"sbi,omitempty"` - Model string `json:"model,omitempty" bson:"model,omitempty"` -} - -// ID returns the ID of the LoadedDevice as UUID. -func (ld LoadedDevice) ID() uuid.UUID { - return uuid.MustParse(ld.DeviceID) + sbiStore southbound.Store } // NewDeviceStore returns a DeviceStore -func NewDeviceStore(pndUUID uuid.UUID, sbiStore southbound.SbiStore) device.Store { +func NewDeviceStore(pndUUID uuid.UUID) device.Store { storeMode := store.GetStoreMode() log.Debugf("StoreMode: %s", storeMode) switch storeMode { case store.Filesystem: - store := NewGenericStore[device.Device]() + store := NewMemoryDeviceStore() - return &store + return store case store.Database: return &DatabaseDeviceStore{ storeName: fmt.Sprintf("device-store-%s.json", pndUUID.String()), - sbiStore: sbiStore, } case store.Memory: - store := NewGenericStore[device.Device]() + store := NewMemoryDeviceStore() - return &store + return store default: return nil } diff --git a/controller/nucleus/errors/errors.go b/controller/nucleus/errors/errors.go index abee04244bf3388beb97f3e0c48271b12bd0d55c..3e38ba8b6e7628b85d3f5811b574a97f5b6f43f3 100644 --- a/controller/nucleus/errors/errors.go +++ b/controller/nucleus/errors/errors.go @@ -195,3 +195,13 @@ type ErrCouldNotCreate struct { func (e ErrCouldNotCreate) Error() string { return fmt.Sprintf("could not create %s", e.StoreName) } + +// ErrCouldNotDelete implements the Error interface and is called if a +// stored item can not be deleted. +type ErrCouldNotDelete struct { + StoreName string +} + +func (e ErrCouldNotDelete) Error() string { + return fmt.Sprintf("could not delete %s", e.StoreName) +} diff --git a/controller/nucleus/genericStore.go b/controller/nucleus/genericService.go similarity index 61% rename from controller/nucleus/genericStore.go rename to controller/nucleus/genericService.go index fe12bdc81b9a63f579ef837d1123134b45cede70..a794928e75c46e5ca8f0f07311f86759b616c313 100644 --- a/controller/nucleus/genericStore.go +++ b/controller/nucleus/genericService.go @@ -10,21 +10,22 @@ type storableConstraint interface { Name() string } -// GenericStore provides a in-memory implementation for multiple stores. -type GenericStore[T storableConstraint] struct { +// GenericService provides a in-memory implementation for multiple stores. +type GenericService[T storableConstraint] struct { Store map[uuid.UUID]T nameLookupTable map[string]uuid.UUID } -// NewGenericStore returns a specific in-memory store for a type T. -func NewGenericStore[T storableConstraint]() GenericStore[T] { - return GenericStore[T]{ +// NewGenericService returns a specific in-memory store for a type T. +func NewGenericService[T storableConstraint]() GenericService[T] { + return GenericService[T]{ Store: make(map[uuid.UUID]T), nameLookupTable: make(map[string]uuid.UUID), } } -func (t *GenericStore[T]) Add(item T) error { +// Add adds a item T +func (t *GenericService[T]) Add(item T) error { _, ok := t.Store[item.ID()] if ok { return nil @@ -36,7 +37,8 @@ func (t *GenericStore[T]) Add(item T) error { return nil } -func (t *GenericStore[T]) Update(item T) error { +// Update updates a item T +func (t *GenericService[T]) Update(item T) error { _, ok := t.Store[item.ID()] if ok { return nil @@ -48,13 +50,15 @@ func (t *GenericStore[T]) Update(item T) error { return nil } -func (t *GenericStore[T]) Delete(item T) error { +// Delete deletes a item T +func (t *GenericService[T]) Delete(item T) error { delete(t.Store, item.ID()) return nil } -func (t *GenericStore[T]) Get(query store.Query) (T, error) { +// Get gets a item T +func (t *GenericService[T]) Get(query store.Query) (T, error) { // First search for direct hit on UUID. item, ok := t.Store[query.ID] if !ok { @@ -75,7 +79,8 @@ func (t *GenericStore[T]) Get(query store.Query) (T, error) { return item, nil } -func (t *GenericStore[T]) GetAll() ([]T, error) { +// GetAll gets all items +func (t *GenericService[T]) GetAll() ([]T, error) { var allItems []T for _, item := range t.Store { diff --git a/controller/nucleus/initialise_test.go b/controller/nucleus/initialise_test.go index 9f9146a1ff9c3fc4b58927480679c4d984092735..9feef90276b7d7a6dddc15102ecf33b0d5f57cb3 100644 --- a/controller/nucleus/initialise_test.go +++ b/controller/nucleus/initialise_test.go @@ -6,7 +6,6 @@ import ( "testing" "code.fbi.h-da.de/danet/gosdn/controller/interfaces/device" - "code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound" "code.fbi.h-da.de/danet/gosdn/controller/store" tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport" @@ -150,14 +149,19 @@ func mockDevice() device.Device { } func newPnd() pndImplementation { - sbiStore := NewGenericStore[southbound.SouthboundInterface]() - deviceStore := NewGenericStore[device.Device]() + sbiStore := NewMemorySbiStore() + deviceStore := NewMemoryDeviceStore() + sbiService := NewSbiService(sbiStore) + deviceService := NewDeviceService( + deviceStore, + sbiService, + ) return pndImplementation{ Name: "default", Description: "default test pnd", - sbic: &sbiStore, - devices: &deviceStore, + sbic: sbiService, + devices: deviceService, changes: store.NewChangeStore(), Id: defaultPndID, } diff --git a/controller/nucleus/memoryDeviceStore.go b/controller/nucleus/memoryDeviceStore.go new file mode 100644 index 0000000000000000000000000000000000000000..536bb6ea4502e5de931b31e3ae95a25c514dda62 --- /dev/null +++ b/controller/nucleus/memoryDeviceStore.go @@ -0,0 +1,111 @@ +package nucleus + +import ( + "encoding/json" + + "code.fbi.h-da.de/danet/gosdn/controller/interfaces/device" + "code.fbi.h-da.de/danet/gosdn/controller/nucleus/errors" + "code.fbi.h-da.de/danet/gosdn/controller/store" +) + +// MemoryDeviceStore provides a in-memory implementation for devices. +type MemoryDeviceStore struct { + Store map[string]device.LoadedDevice + nameLookupTable map[string]string +} + +// NewMemoryDeviceStore returns a specific in-memory store for devices. +func NewMemoryDeviceStore() device.Store { + return &MemoryDeviceStore{ + Store: make(map[string]device.LoadedDevice), + nameLookupTable: make(map[string]string), + } +} + +// Add adds a item to the store. +func (t *MemoryDeviceStore) Add(item device.Device) error { + var device device.LoadedDevice + + b, err := json.Marshal(item) + if err != nil { + return err + } + err = json.Unmarshal(b, &device) + if err != nil { + return err + } + + _, ok := t.Store[device.DeviceID] + if ok { + return nil + } + + t.Store[device.DeviceID] = device + t.nameLookupTable[item.Name()] = device.DeviceID + + return nil +} + +// Update updates a existing device. +func (t *MemoryDeviceStore) Update(item device.Device) error { + _, ok := t.Store[item.ID().String()] + if ok { + return nil + } + + var device device.LoadedDevice + + b, err := json.Marshal(item) + if err != nil { + return err + } + err = json.Unmarshal(b, &device) + if err != nil { + return err + } + + t.Store[item.ID().String()] = device + t.nameLookupTable[item.Name()] = item.ID().String() + + return nil +} + +// Delete deletes a device from the device store. +func (t *MemoryDeviceStore) Delete(item device.Device) error { + delete(t.Store, item.ID().String()) + + return nil +} + +// Get takes a Device's UUID or name and returns the Device. +func (t *MemoryDeviceStore) Get(query store.Query) (device.LoadedDevice, error) { + // First search for direct hit on UUID. + item, ok := t.Store[query.ID.String()] + if !ok { + // Second search for name + id, ok := t.nameLookupTable[query.Name] + if !ok { + return item, errors.ErrCouldNotFind{StoreName: deviceStoreName} + } + + item, ok := t.Store[id] + if !ok { + return item, errors.ErrCouldNotFind{StoreName: deviceStoreName} + } + + return item, nil + } + + return item, nil +} + +// GetAll returns all stored devices. +func (t *MemoryDeviceStore) GetAll() ([]device.LoadedDevice, error) { + var allItems []device.LoadedDevice + + for _, item := range t.Store { + allItems = append(allItems, item) + } + + return allItems, nil +} diff --git a/controller/nucleus/memorySbiStore.go b/controller/nucleus/memorySbiStore.go new file mode 100644 index 0000000000000000000000000000000000000000..d9f1245026c8bbca4ee462d64bf25c70d6b40878 --- /dev/null +++ b/controller/nucleus/memorySbiStore.go @@ -0,0 +1,111 @@ +package nucleus + +import ( + "encoding/json" + + "code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound" + "code.fbi.h-da.de/danet/gosdn/controller/nucleus/errors" + "code.fbi.h-da.de/danet/gosdn/controller/store" +) + +// MemorySbiStore provides a in-memory implementation for sbis. +type MemorySbiStore struct { + Store map[string]southbound.LoadedSbi + nameLookupTable map[string]string +} + +// NewMemorySbiStore returns a specific in-memory store for a type T. +func NewMemorySbiStore() southbound.Store { + return &MemorySbiStore{ + Store: make(map[string]southbound.LoadedSbi), + nameLookupTable: make(map[string]string), + } +} + +// Add adds a item to the store. +func (t *MemorySbiStore) Add(item southbound.SouthboundInterface) error { + var sbi southbound.LoadedSbi + + b, err := json.Marshal(item) + if err != nil { + return err + } + err = json.Unmarshal(b, &sbi) + if err != nil { + return err + } + + _, ok := t.Store[sbi.ID] + if ok { + return nil + } + + t.Store[sbi.ID] = sbi + t.nameLookupTable[item.Name()] = sbi.ID + + return nil +} + +// Update updates a existing sbi. +func (t *MemorySbiStore) Update(item southbound.SouthboundInterface) error { + _, ok := t.Store[item.ID().String()] + if ok { + return nil + } + + var sbi southbound.LoadedSbi + + b, err := json.Marshal(item) + if err != nil { + return err + } + err = json.Unmarshal(b, &sbi) + if err != nil { + return err + } + + t.Store[item.ID().String()] = sbi + t.nameLookupTable[item.Name()] = item.ID().String() + + return nil +} + +// Delete deletes a sbi from the store. +func (t *MemorySbiStore) Delete(item southbound.SouthboundInterface) error { + delete(t.Store, item.ID().String()) + + return nil +} + +// Get takes a sbi's UUID or name and returns the sbi. +func (t *MemorySbiStore) Get(query store.Query) (southbound.LoadedSbi, error) { + // First search for direct hit on UUID. + item, ok := t.Store[query.ID.String()] + if !ok { + // Second search for name + id, ok := t.nameLookupTable[query.Name] + if !ok { + return item, errors.ErrCouldNotDelete{StoreName: sbiStoreName} + } + + item, ok := t.Store[id] + if !ok { + return item, errors.ErrCouldNotFind{StoreName: sbiStoreName} + } + + return item, nil + } + + return item, nil +} + +// GetAll returns all stored sbis. +func (t *MemorySbiStore) GetAll() ([]southbound.LoadedSbi, error) { + var allItems []southbound.LoadedSbi + + for _, item := range t.Store { + allItems = append(allItems, item) + } + + return allItems, nil +} diff --git a/controller/nucleus/principalNetworkDomain.go b/controller/nucleus/principalNetworkDomain.go index 9f24bcc9f03017d2a79eba32cb1c50ba7c1a1d5f..3d23862bcc27251e1d8ddb82893b2bb9675b3ca6 100644 --- a/controller/nucleus/principalNetworkDomain.go +++ b/controller/nucleus/principalNetworkDomain.go @@ -52,6 +52,14 @@ func NewPND( callback func(uuid.UUID, chan device.Details), ) (networkdomain.NetworkDomain, error) { sbiStore := NewSbiStore(id) + deviceStore := NewDeviceStore(id) + changeStore := store.NewChangeStore() + + sbiService := NewSbiService(sbiStore) + deviceService := NewDeviceService( + deviceStore, + sbiService, + ) changeStore, ok := changeStoreMap[id] if !ok { @@ -62,8 +70,8 @@ func NewPND( pnd := &pndImplementation{ Name: name, Description: description, - sbic: sbiStore, - devices: NewDeviceStore(id, sbiStore), + sbic: sbiService, + devices: deviceService, changes: changeStore, Id: id, @@ -87,8 +95,8 @@ func NewPND( type pndImplementation struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` - sbic southbound.SbiStore - devices device.Store + sbic southbound.Service + devices device.Service changes *store.ChangeStore //nolint Id uuid.UUID `json:"id,omitempty"` diff --git a/controller/nucleus/principalNetworkDomain_test.go b/controller/nucleus/principalNetworkDomain_test.go index f6d58d02dab9cb0ebeb4c3a547a75f03242bcd08..2045bd626806c935b25c99e37b2eac0875ca46ac 100644 --- a/controller/nucleus/principalNetworkDomain_test.go +++ b/controller/nucleus/principalNetworkDomain_test.go @@ -195,8 +195,8 @@ func Test_pndImplementation_Destroy(t *testing.T) { type fields struct { name string description string - sbi southbound.SbiStore - devices device.Store + sbi southbound.Service + devices device.Service } tests := []struct { name string @@ -254,23 +254,6 @@ func Test_pndImplementation_GetName(t *testing.T) { } } -// func Test_pndImplementation_GetSBIs(t *testing.T) { -// pnd := newPnd() -// tests := []struct { -// name string -// want []southbound.SouthboundInterface -// }{ -// {name: "default", want: []southbound.SouthboundInterface{}}, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// if got, _ := pnd.GetSBIs(); !reflect.DeepEqual(got, tt.want) { -// t.Errorf("GetSBIs() = %v, want %v", got, tt.want) -// } -// }) -// } -// } - func Test_pndImplementation_MarshalDevice(t *testing.T) { type args struct { uuid uuid.UUID @@ -292,10 +275,19 @@ func Test_pndImplementation_MarshalDevice(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pnd := newPnd() + sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) + if err != nil { + t.Error("could not create sbi") + } + + err = pnd.addSbi(sbi) + if err != nil { + t.Error("could not add sbi") + } d := &CommonDevice{ UUID: tt.args.uuid, Model: &openconfig.Device{}, - sbi: nil, + sbi: sbi, transport: nil, } if err := pnd.addDevice(d); err != nil { @@ -327,25 +319,28 @@ func Test_pndImplementation_RemoveDevice(t *testing.T) { }{ {name: "default", args: args{uuid: did}, wantErr: false}, {name: "fails", args: args{uuid: uuid.New()}, wantErr: true}, - {name: "fails empty", args: args{uuid: did}, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pnd := newPnd() - if tt.name != "fails empty" { - sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) - if err != nil { - t.Error(err) - return - } - d := &CommonDevice{ - UUID: did, - sbi: sbi, - } - if err := pnd.addDevice(d); err != nil { - t.Error(err) - } + sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) + if err != nil { + t.Error("could not create sbi") + } + + err = pnd.addSbi(sbi) + if err != nil { + t.Error("could not add sbi") + } + d := &CommonDevice{ + UUID: did, + Model: &openconfig.Device{}, + sbi: sbi, + transport: nil, + } + if err := pnd.addDevice(d); err != nil { + t.Error(err) } if err := pnd.RemoveDevice(tt.args.uuid); (err != nil) != tt.wantErr { t.Errorf("RemoveDevice() error = %v, wantErr %v", err, tt.wantErr) @@ -355,6 +350,67 @@ func Test_pndImplementation_RemoveDevice(t *testing.T) { } func Test_pndImplementation_RemoveSbi(t *testing.T) { + type args struct { + id uuid.UUID + } + tests := []struct { + name string + args args + wantErr bool + }{ + {name: "default", args: args{id: defaultSbiID}, wantErr: false}, + {name: "fails", args: args{id: uuid.New()}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sbiStore := NewSbiStore(defaultPndID) + deviceStore := NewDeviceStore(defaultPndID) + sbiService := NewSbiService(sbiStore) + deviceService := NewDeviceService(deviceStore, sbiService) + + pnd := &pndImplementation{ + Name: "test-remove-sbi", + Description: "test-remove-sbi", + sbic: sbiService, + devices: deviceService, + Id: defaultPndID, + } + + sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG, defaultSbiID) + if err != nil { + t.Error("could not create sbi") + } + + err = pnd.addSbi(sbi) + if err != nil { + t.Error("could not add sbi") + } + + if err := pnd.RemoveSbi(tt.args.id); (err != nil) != tt.wantErr { + t.Errorf("RemoveSbi() error = %v, wantErr %v", err, tt.wantErr) + } + + foundSbi, _ := pnd.sbic.Get(store.Query{ID: tt.args.id}) + if foundSbi != nil { + t.Errorf("RemoveSbi() SBI still in SBI store %v", pnd.sbic) + } + + if tt.name == "exclusively remove associated devices" { + allDevices, _ := pnd.devices.GetAll() + if len(allDevices) != 1 { + t.Errorf("RemoveSbi() non associated devices should remain in the storage %v", allDevices) + } + } else { + allDevices, _ := pnd.devices.GetAll() + if len(allDevices) != 0 { + t.Errorf("RemoveSbi() associated devices have not been removed correctly %v", len(allDevices)) + } + } + }) + } +} + +func Test_pndImplementation_RemoveSbiWithAssociatedDevices(t *testing.T) { opts := &tpb.TransportOption{ TransportOption: &tpb.TransportOption_GnmiTransportOption{ GnmiTransportOption: &tpb.GnmiTransportOption{}, @@ -368,49 +424,58 @@ func Test_pndImplementation_RemoveSbi(t *testing.T) { args args wantErr bool }{ - {name: "default", args: args{id: defaultSbiID}, wantErr: false}, - {name: "fails", args: args{id: uuid.New()}, wantErr: true}, - {name: "fails empty", args: args{id: defaultSbiID}, wantErr: true}, {name: "exclusively remove associated devices", args: args{id: defaultSbiID}, wantErr: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sbiStore := NewSbiStore(defaultPndID) + deviceStore := NewDeviceStore(defaultPndID) + sbiService := NewSbiService(sbiStore) + deviceService := NewDeviceService(deviceStore, sbiService) pnd := &pndImplementation{ Name: "test-remove-sbi", Description: "test-remove-sbi", - sbic: sbiStore, - devices: NewDeviceStore(defaultPndID, sbiStore), + sbic: sbiService, + devices: deviceService, Id: defaultPndID, } - if tt.name != "fails empty" && tt.name != "fails" { - if err := pnd.addSbi(&OpenConfig{id: defaultSbiID}); err != nil { - t.Error(err) - } - if err := pnd.AddDevice("associatedDevice", opts, tt.args.id); err != nil { + + sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG, defaultSbiID) + if err != nil { + t.Error("could not create sbi") + } + + err = pnd.addSbi(sbi) + if err != nil { + t.Error("could not add sbi") + } + + if err := pnd.addSbi(&OpenConfig{id: defaultSbiID}); err != nil { + t.Error(err) + } + if err := pnd.AddDevice("associatedDevice", opts, tt.args.id); err != nil { + t.Error(err) + } + if err := pnd.AddDevice("associatedDevice2", opts, tt.args.id); err != nil { + t.Error(err) + } + if tt.name == "exclusively remove associated devices" { + newID := uuid.New() + if err := pnd.addSbi(&OpenConfig{id: newID}); err != nil { t.Error(err) } - if err := pnd.AddDevice("associatedDevice2", opts, tt.args.id); err != nil { + if err := pnd.AddDevice("associatedDevice2", opts, newID); err != nil { t.Error(err) } - if tt.name == "exclusively remove associated devices" { - newID := uuid.New() - if err := pnd.addSbi(&OpenConfig{id: newID}); err != nil { - t.Error(err) - } - if err := pnd.AddDevice("associatedDevice2", opts, newID); err != nil { - t.Error(err) - } - } } if err := pnd.RemoveSbi(tt.args.id); (err != nil) != tt.wantErr { t.Errorf("RemoveSbi() error = %v, wantErr %v", err, tt.wantErr) } - sbi, _ := pnd.sbic.Get(store.Query{ID: tt.args.id}) - if sbi != nil { + foundSbi, _ := pnd.sbic.Get(store.Query{ID: tt.args.id}) + if foundSbi != nil { t.Errorf("RemoveSbi() SBI still in SBI store %v", pnd.sbic) } @@ -462,14 +527,42 @@ func Test_pndImplementation_Request(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - deviceWithMockTransport := mockDevice() - pnd := newPnd() - transport := deviceWithMockTransport.Transport().(*mocks.Transport) + sbiService := NewGenericService[southbound.SouthboundInterface]() + deviceService := NewGenericService[device.Device]() + + pnd := pndImplementation{ + Name: "default", + Description: "default test pnd", + sbic: &sbiService, + devices: &deviceService, + changes: store.NewChangeStore(), + Id: defaultPndID, + } + + sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) + if err != nil { + t.Error("could not create sbi") + } + + err = pnd.addSbi(sbi) + if err != nil { + t.Error("could not add sbi") + } + + transport := mocks.Transport{} transport.On("Get", mockContext, mock.Anything).Return(&gpb.GetResponse{}, tt.args.rErr) transport.On("ProcessResponse", mock.Anything, mock.Anything, mock.Anything).Return(tt.args.rErr) + + deviceWithMockTransport := &CommonDevice{ + UUID: mdid, + Model: &openconfig.Device{}, + sbi: sbi, + transport: &transport, + } + _ = pnd.addDevice(deviceWithMockTransport) - _, err := pnd.Request(tt.args.uuid, tt.args.path) + _, err = pnd.Request(tt.args.uuid, tt.args.path) if (err != nil) != tt.wantErr { t.Errorf("Request() error = %v, wantErr %v", err, tt.wantErr) } @@ -518,17 +611,41 @@ func Test_pndImplementation_RequestAll(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - deviceWithMockTransport := mockDevice() - pnd := newPnd() - tr := deviceWithMockTransport.Transport().(*mocks.Transport) - tr.On("Get", mockContext, mock.Anything).Return(&gpb.GetResponse{}, tt.args.rErr) - tr.On("ProcessResponse", mock.Anything, mock.Anything, mock.Anything).Return(tt.args.rErr) + sbiService := NewGenericService[southbound.SouthboundInterface]() + deviceService := NewGenericService[device.Device]() - _ = pnd.addDevice(deviceWithMockTransport) - if err := pnd.RequestAll(tt.args.path); (err != nil) != tt.wantErr { - t.Errorf("RequestAll() error = %v, wantErr %v", err, tt.wantErr) + pnd := pndImplementation{ + Name: "default", + Description: "default test pnd", + sbic: &sbiService, + devices: &deviceService, + changes: store.NewChangeStore(), + Id: defaultPndID, + } + + sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) + if err != nil { + t.Error("could not create sbi") + } + + err = pnd.addSbi(sbi) + if err != nil { + t.Error("could not add sbi") } + transport := mocks.Transport{} + transport.On("Get", mockContext, mock.Anything).Return(&gpb.GetResponse{}, tt.args.rErr) + transport.On("ProcessResponse", mock.Anything, mock.Anything, mock.Anything).Return(tt.args.rErr) + + deviceWithMockTransport := &CommonDevice{ + UUID: mdid, + Model: &openconfig.Device{}, + sbi: sbi, + transport: &transport, + } + + _ = pnd.addDevice(deviceWithMockTransport) + device, _ := pnd.devices.Get(store.Query{ID: mdid}) if device == nil { return @@ -672,7 +789,12 @@ func Test_pndImplementation_GetDevice(t *testing.T) { t.Errorf("NewSBI() error = %v", err) return } - d, err := NewDevice("", uuid.Nil, newGnmiTransportOptions(), sbi) + err = pnd.addSbi(sbi) + if err != nil { + t.Error(err) + return + } + d, err := NewDevice("default", did, newGnmiTransportOptions(), sbi) if err != nil { t.Error(err) return @@ -681,6 +803,7 @@ func Test_pndImplementation_GetDevice(t *testing.T) { t.Error(err) return } + type args struct { uuid uuid.UUID } @@ -692,7 +815,7 @@ func Test_pndImplementation_GetDevice(t *testing.T) { }{ { name: "default", - args: args{uuid: d.ID()}, + args: args{uuid: did}, want: sbi.Schema().Root, wantErr: false, }, @@ -720,18 +843,23 @@ func Test_pndImplementation_GetDevice(t *testing.T) { } func Test_pndImplementation_GetDeviceByName(t *testing.T) { - p := newPnd() + pnd := newPnd() sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("NewSBI() error = %v", err) return } - d, err := NewDevice("my-device", uuid.Nil, newGnmiTransportOptions(), sbi) + err = pnd.addSbi(sbi) + if err != nil { + t.Error(err) + return + } + d, err := NewDevice("my-device", did, newGnmiTransportOptions(), sbi) if err != nil { t.Error(err) return } - if err = p.addDevice(d); err != nil { + if err = pnd.addDevice(d); err != nil { t.Error(err) return } @@ -759,7 +887,7 @@ func Test_pndImplementation_GetDeviceByName(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - foundDevice, err := p.GetDevice(tt.args.name) + foundDevice, err := pnd.GetDevice(tt.args.name) if (err != nil) != tt.wantErr { t.Errorf("GetDeviceByName() error = %v, wantErr %v", err, tt.wantErr) return @@ -790,7 +918,18 @@ func Test_pndImplementation_Confirm(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - pnd := newPnd() + sbiService := NewGenericService[southbound.SouthboundInterface]() + deviceService := NewGenericService[device.Device]() + + pnd := pndImplementation{ + Name: "default", + Description: "default test pnd", + sbic: &sbiService, + devices: &deviceService, + changes: store.NewChangeStore(), + Id: defaultPndID, + } + d := mockDevice() tr := d.Transport().(*mocks.Transport) tr.On("Set", mockContext, mock.Anything, mock.Anything).Return(nil) diff --git a/controller/nucleus/sbiService.go b/controller/nucleus/sbiService.go new file mode 100644 index 0000000000000000000000000000000000000000..a92a3be32b159ee32173bbf295adab6ad7a6e61f --- /dev/null +++ b/controller/nucleus/sbiService.go @@ -0,0 +1,84 @@ +package nucleus + +import ( + "code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound" + "code.fbi.h-da.de/danet/gosdn/controller/store" + "github.com/google/uuid" +) + +// SbiService provides a sbi service implementation. +type SbiService struct { + sbiStore southbound.Store +} + +// NewSbiService creates a sbi service. +func NewSbiService(sbiStore southbound.Store) southbound.Service { + return &SbiService{ + sbiStore: sbiStore, + } +} + +// Get takes a SBI's UUID or name and returns the SBI. +func (s *SbiService) Get(query store.Query) (southbound.SouthboundInterface, error) { + loadedSbi, err := s.sbiStore.Get(query) + if err != nil { + return nil, err + } + + device, err := s.createSbiFromStore(loadedSbi) + if err != nil { + return nil, err + } + + return device, nil +} + +// GetAll returns all stored SBIs. +func (s *SbiService) GetAll() ([]southbound.SouthboundInterface, error) { + var sbis []southbound.SouthboundInterface + + loadedSbis, err := s.sbiStore.GetAll() + if err != nil { + return nil, err + } + + for _, loadedSbi := range loadedSbis { + sbi, err := s.createSbiFromStore(loadedSbi) + if err != nil { + return nil, err + } + + sbis = append(sbis, sbi) + } + + return sbis, nil +} + +// Add adds a sbi to the sbi store. +func (s *SbiService) Add(sbiToAdd southbound.SouthboundInterface) error { + err := s.sbiStore.Add(sbiToAdd) + if err != nil { + return err + } + + return nil +} + +// Delete deletes a sbi from the sbi store. +func (s *SbiService) Delete(sbiToDelete southbound.SouthboundInterface) error { + err := s.sbiStore.Delete(sbiToDelete) + if err != nil { + return err + } + + return nil +} + +func (s *SbiService) createSbiFromStore(loadedSbi southbound.LoadedSbi) (southbound.SouthboundInterface, error) { + newSbi, err := NewSBI(loadedSbi.Type, uuid.MustParse(loadedSbi.ID)) + if err != nil { + return nil, err + } + + return newSbi, nil +} diff --git a/controller/nucleus/sbiService_test.go b/controller/nucleus/sbiService_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d26d6d72e1096326926d1033594645e14f09f603 --- /dev/null +++ b/controller/nucleus/sbiService_test.go @@ -0,0 +1,95 @@ +package nucleus + +import ( + "testing" + + "code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound" + "code.fbi.h-da.de/danet/gosdn/controller/store" + "github.com/google/uuid" +) + +func getMockSbi(sbiID uuid.UUID) southbound.SouthboundInterface { + return &OpenConfig{id: sbiID} +} + +func getSbiTestStores(t *testing.T, sbiID uuid.UUID) (southbound.Service, southbound.SouthboundInterface) { + sbiStore := NewMemorySbiStore() + sbiService := NewSbiService(sbiStore) + + mockSbi := getMockSbi(sbiID) + + err := sbiService.Add(mockSbi) + if err != nil { + t.Error("could not create sbi") + } + + return sbiService, mockSbi +} + +func TestSbiService_Get(t *testing.T) { + sbiID := uuid.New() + + sbiService, mockSbi := getSbiTestStores(t, sbiID) + + sbi, err := sbiService.Get(store.Query{ + ID: mockSbi.ID(), + Name: mockSbi.Name(), + }) + if err != nil { + t.Error("could not get sbi") + } + + if mockSbi.ID() != sbi.ID() { + t.Errorf("Expected ID=%s, got %s", mockSbi.ID(), sbi.ID()) + } +} + +func TestSbiService_Delete(t *testing.T) { + sbiID := uuid.New() + + sbiService, mockSbi := getSbiTestStores(t, sbiID) + + sbi, err := sbiService.Get(store.Query{ + ID: mockSbi.ID(), + Name: mockSbi.Name(), + }) + if err != nil { + t.Error("could not get sbi") + } + + err = sbiService.Delete(sbi) + if err != nil { + t.Error("could not delete sbi") + } +} + +func TestSbiService_GetAll(t *testing.T) { + sbiID := uuid.New() + sbiID2 := uuid.New() + + sbiService, mockSbi := getSbiTestStores(t, sbiID) + + _, err := sbiService.Get(store.Query{ + ID: mockSbi.ID(), + Name: mockSbi.Name(), + }) + if err != nil { + t.Error("could not get sbi") + } + + mockSbi2 := getMockSbi(sbiID2) + + err = sbiService.Add(mockSbi2) + if err != nil { + t.Error("could not add sbi") + } + + sbis, err := sbiService.GetAll() + if err != nil { + t.Error("could not get all devices") + } + + if len(sbis) != 2 { + t.Errorf("Expected len(devices)=2, got %d", len(sbis)) + } +} diff --git a/controller/nucleus/sbiStore.go b/controller/nucleus/sbiStore.go index 16fdb7bbe971fb2a18b7ea18e1fd9f3fb307f042..7038980cfb26fec86fa0c4d348544f3eb25df509 100644 --- a/controller/nucleus/sbiStore.go +++ b/controller/nucleus/sbiStore.go @@ -3,7 +3,6 @@ package nucleus import ( "fmt" - spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound" "code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound" "code.fbi.h-da.de/danet/gosdn/controller/store" @@ -19,31 +18,23 @@ type SbiStore struct { sbiStoreName string } -// LoadedSbi represents a Southbound Interface that was loaded by using -// the Load() method of the SbiStore. -type LoadedSbi struct { - ID string `json:"_id" bson:"_id,omitempty"` - Type spb.Type `bson:"type"` - Path string `json:"path,omitempty"` -} - // NewSbiStore returns a sbiStore -func NewSbiStore(pndUUID uuid.UUID) southbound.SbiStore { +func NewSbiStore(pndUUID uuid.UUID) southbound.Store { storeMode := store.GetStoreMode() switch storeMode { case store.Filesystem: - store := NewGenericStore[southbound.SouthboundInterface]() + store := NewMemorySbiStore() - return &store + return store case store.Database: return &DatabaseSbiStore{ sbiStoreName: fmt.Sprintf("sbi-store-%s.json", pndUUID.String()), } case store.Memory: - store := NewGenericStore[southbound.SouthboundInterface]() + store := NewMemorySbiStore() - return &store + return store default: return nil } diff --git a/controller/nucleus/southbound.go b/controller/nucleus/southbound.go index 2c18dd0922a9ed401f16782e2c4a7715eb3bbe60..927900849076da638a2623a8a5da4ca1123841ff 100644 --- a/controller/nucleus/southbound.go +++ b/controller/nucleus/southbound.go @@ -1,6 +1,7 @@ package nucleus import ( + "encoding/json" "path/filepath" "code.fbi.h-da.de/danet/gosdn/controller/nucleus/errors" @@ -294,7 +295,29 @@ func (p *SouthboundPlugin) Update() error { return nil } -// MarshalBSON implements the MarshalBSON interface to store a device as BSON +// MarshalJSON implements the MarshalJSON interface to store a sbi as JSON +func (p *SouthboundPlugin) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"_id"` + Type spb.Type `json:"type"` + }{ + ID: p.sbi.ID().String(), + Type: p.Type(), + }) +} + +// MarshalJSON implements the MarshalJSON interface to store a sbi as JSON +func (oc *OpenConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"_id"` + Type spb.Type `json:"type"` + }{ + ID: oc.id.String(), + Type: oc.Type(), + }) +} + +// MarshalBSON implements the MarshalBSON interface to store a sbi as BSON func (p *SouthboundPlugin) MarshalBSON() ([]byte, error) { return bson.Marshal(&struct { ID string `bson:"_id"` @@ -305,7 +328,7 @@ func (p *SouthboundPlugin) MarshalBSON() ([]byte, error) { }) } -// MarshalBSON implements the MarshalBSON interface to store a device as BSON +// MarshalBSON implements the MarshalBSON interface to store a sbi as BSON func (oc *OpenConfig) MarshalBSON() ([]byte, error) { return bson.Marshal(&struct { ID string `bson:"_id"`