diff --git a/.gitignore b/.gitignore index d5a3ac156facca5eb8e2a3451f4b6e0ef3a6955e..c76484002e90465087349e42f88690fcc99b26e1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ build-tools/ # test files report.xml +test/plugin/**/*.so nucleus/util/proto/*_test # Binary @@ -31,3 +32,4 @@ gosdn # persistent data **/stores/** +plugins diff --git a/api/grpc.go b/api/grpc.go index a9ef08f46326e98f8ae012ddef9e23a7426a1227..f0880b4e8249a182ee5aad3baed4a47355e2bf59 100644 --- a/api/grpc.go +++ b/api/grpc.go @@ -124,6 +124,20 @@ func GetPnds(addr string, args ...string) (*pb.GetPndListResponse, error) { return coreClient.GetPndList(ctx, req) } +// deletePnd requests a deletion of the provided PND. +func deletePnd(addr string, pid string) (*pb.DeletePndResponse, error) { + coreClient, err := nbi.CoreClient(addr, dialOptions...) + if err != nil { + return nil, err + } + ctx := context.Background() + req := &pb.DeletePndRequest{ + Timestamp: time.Now().UnixNano(), + Pid: pid, + } + return coreClient.DeletePnd(ctx, req) +} + // getChanges requests all pending and unconfirmed changes from the controller func getChanges(addr, pnd string) (*ppb.GetChangeListResponse, error) { ctx := context.Background() diff --git a/api/initialise_test.go b/api/initialise_test.go index 4dbb90a3e363242f7e1a6afb8fe1ab4eb9594574..078588fce5d88f0d673f9e2e6de0db55d3f47777 100644 --- a/api/initialise_test.go +++ b/api/initialise_test.go @@ -53,8 +53,6 @@ func bootstrapUnitTest() { } lis = bufconn.Listen(bufSize) s := grpc.NewServer() - pndStore = store.NewPndStore() - sbiStore = store.NewSbiStore() changeUUID, err := uuid.Parse(changeID) if err != nil { @@ -76,6 +74,9 @@ func bootstrapUnitTest() { log.Fatal(err) } + pndStore = store.NewPndStore() + sbiStore = store.NewSbiStore(pndUUID) + mockChange := &mocks.Change{} mockChange.On("Age").Return(time.Hour) mockChange.On("State").Return(ppb.ChangeState_CHANGE_STATE_INCONSISTENT) diff --git a/api/pnd.go b/api/pnd.go index ea6e78a8f02e2c1643143940fcc18628d9609630..9b647906ac6ca064f4ee0efb91cae9d34f30c142 100644 --- a/api/pnd.go +++ b/api/pnd.go @@ -1,11 +1,10 @@ package api import ( + "code.fbi.h-da.de/danet/api/go/gosdn/core" ppb "code.fbi.h-da.de/danet/api/go/gosdn/pnd" tpb "code.fbi.h-da.de/danet/api/go/gosdn/transport" "code.fbi.h-da.de/danet/gosdn/interfaces/change" - "code.fbi.h-da.de/danet/gosdn/interfaces/device" - "code.fbi.h-da.de/danet/gosdn/interfaces/networkdomain" "code.fbi.h-da.de/danet/gosdn/interfaces/southbound" "code.fbi.h-da.de/danet/gosdn/interfaces/store" "code.fbi.h-da.de/danet/gosdn/nucleus/errors" @@ -23,7 +22,7 @@ type PrincipalNetworkDomainAdapter struct { // NewAdapter creates a PND Adapter. It requires a valid PND UUID and a reachable // goSDN endpoint. -func NewAdapter(id, endpoint string) (networkdomain.NetworkDomain, error) { +func NewAdapter(id, endpoint string) (*PrincipalNetworkDomainAdapter, error) { pid, err := uuid.Parse(id) if err != nil { return nil, err @@ -51,41 +50,53 @@ func (p *PrincipalNetworkDomainAdapter) RemoveSbi(uuid.UUID) error { // AddDevice adds a new device to the controller. The device name is optional. // If no name is provided a name will be generated upon device creation. -func (p *PrincipalNetworkDomainAdapter) AddDevice(name string, opts *tpb.TransportOption, sid uuid.UUID) error { +func (p *PrincipalNetworkDomainAdapter) AddDevice(name string, opts *tpb.TransportOption, sid uuid.UUID) (*ppb.SetOndListResponse, error) { resp, err := addDevice(p.endpoint, name, opts, sid, p.ID()) if err != nil { - return err + return nil, err } - log.Info(resp) - return nil -} - -// AddDeviceFromStore adds a new device from store to the controller. Currently not implemented -func (p *PrincipalNetworkDomainAdapter) AddDeviceFromStore(name string, did uuid.UUID, opts *tpb.TransportOption, sid uuid.UUID) error { - return &errors.ErrNotYetImplemented{} + return resp, nil } // GetDevice requests one or multiple devices belonging to a given // PrincipalNetworkDomain from the controller. If no device identifier // is provided, all devices are requested. -func (p *PrincipalNetworkDomainAdapter) GetDevice(identifier string) (device.Device, error) { - resp, err := getDevice(p.endpoint, p.id.String(), identifier) +func (p *PrincipalNetworkDomainAdapter) GetDevice(identifier ...string) ([]*ppb.OrchestratedNetworkingDevice, error) { + resp, err := getDevice(p.endpoint, p.id.String(), identifier...) if err != nil { return nil, err } - // TODO: Parse into device.Device - log.Info(resp) - return nil, nil + return resp.Ond, nil +} + +// GetDevices requests all devices belonging to the PrincipalNetworkDomain +// attached to this adapter. +// TODO: this function could be removed, since GetDevice() with no identifier +// provided returns all devices too. +func (p *PrincipalNetworkDomainAdapter) GetDevices() ([]*ppb.OrchestratedNetworkingDevice, error) { + resp, err := getDevices(p.endpoint, p.id.String()) + if err != nil { + return nil, err + } + return resp.Ond, nil } -// RemoveDevice removes a device from the PND Adapter -func (p *PrincipalNetworkDomainAdapter) RemoveDevice(did uuid.UUID) error { +// RemoveDevice removes a device from the controller +func (p *PrincipalNetworkDomainAdapter) RemoveDevice(did uuid.UUID) (*ppb.DeleteOndResponse, error) { resp, err := deleteDevice(p.endpoint, p.id.String(), did.String()) if err != nil { - return err + return nil, err } - log.Info(resp) - return nil + return resp, nil +} + +// RemovePnd removes a PND from the controller +func (p *PrincipalNetworkDomainAdapter) RemovePnd(pid uuid.UUID) (*core.DeletePndResponse, error) { + resp, err := deletePnd(p.endpoint, pid.String()) + if err != nil { + return nil, err + } + return resp, nil } // Devices sends an API call to the controller requesting the UUIDs of all @@ -159,6 +170,11 @@ func (p *PrincipalNetworkDomainAdapter) ID() uuid.UUID { return p.id } +// Endpoint returns the PND Adapter's endpoint +func (p *PrincipalNetworkDomainAdapter) Endpoint() string { + return p.endpoint +} + // PendingChanges sends an API call to the controller requesting // the UUIDs of all pending changes func (p *PrincipalNetworkDomainAdapter) PendingChanges() []uuid.UUID { diff --git a/api/pnd_test.go b/api/pnd_test.go index 0a2e9d00ccdde690244e0ddcec8395f1feaff6c1..de515ca9d11ea4b6f34392e36c4a2c7854680d1d 100644 --- a/api/pnd_test.go +++ b/api/pnd_test.go @@ -148,7 +148,7 @@ func TestPrincipalNetworkDomainAdapter_AddDevice(t *testing.T) { id: tt.fields.id, endpoint: tt.fields.endpoint, } - if err := p.AddDevice(tt.args.name, tt.args.opts, tt.args.sid); (err != nil) != tt.wantErr { + if _, err := p.AddDevice(tt.args.name, tt.args.opts, tt.args.sid); (err != nil) != tt.wantErr { t.Errorf("PrincipalNetworkDomainAdapter.AddDevice() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -212,7 +212,7 @@ func TestPrincipalNetworkDomainAdapter_RemoveDevice(t *testing.T) { id: tt.fields.id, endpoint: tt.fields.endpoint, } - if err := p.RemoveDevice(tt.args.did); (err != nil) != tt.wantErr { + if _, err := p.RemoveDevice(tt.args.did); (err != nil) != tt.wantErr { t.Errorf("PrincipalNetworkDomainAdapter.RemoveDevice() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/cmd/root.go b/cmd/root.go index 47cbec82915ad111268370cea0dbb191b2265a7e..91866fb4176abc2907f80fb9e592f5d89593426b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -48,6 +48,7 @@ var cfgFile string var loglevel string var grpcPort string var csbiOrchestrator string +var pluginFolder string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ @@ -79,6 +80,7 @@ func init() { rootCmd.Flags().StringVar(&grpcPort, "grpc-port", "", "port for gRPC NBI") rootCmd.Flags().StringVar(&csbiOrchestrator, "csbi-orchestrator", "", "csbi orchestrator address") + rootCmd.Flags().StringVar(&pluginFolder, "plugin-folder", "", "folder holding all goSDN specific plugins") } const ( @@ -111,8 +113,11 @@ func initConfig() { if err := viper.BindPFlags(rootCmd.Flags()); err != nil { log.Fatal(err) } + + // Set default configuration values viper.SetDefault("socket", ":55055") viper.SetDefault("csbi-orchestrator", "localhost:55056") + viper.SetDefault("plugin-folder", "plugins") ll := viper.GetString("GOSDN_LOG") if ll != "" { diff --git a/go.mod b/go.mod index e26fa1d1dfc768d26a78aeb57f4fac9bd53c7a73..b23e2355eceb2fa39da42e4b4a901614ba8c7307 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module code.fbi.h-da.de/danet/gosdn go 1.17 require ( - code.fbi.h-da.de/danet/api v0.2.5-0.20220301081709-d68d321135a7 + code.fbi.h-da.de/danet/api v0.2.5-0.20220309155741-8041238ad200 code.fbi.h-da.de/danet/forks/goarista v0.0.0-20210709163519-47ee8958ef40 code.fbi.h-da.de/danet/forks/google v0.0.0-20210709163519-47ee8958ef40 code.fbi.h-da.de/danet/yang-models v0.1.0 @@ -23,6 +23,7 @@ require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c google.golang.org/grpc v1.43.0 google.golang.org/protobuf v1.27.1 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) require ( @@ -55,5 +56,4 @@ require ( google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect gopkg.in/ini.v1 v1.64.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 9321fe8926b9188a55d5077c218fc5b8ffff2a7f..7ba1b0af190a7b51ffbc68b1a3cb641039733a0e 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,10 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= code.fbi.h-da.de/danet/api v0.2.5-0.20220301081709-d68d321135a7 h1:DxcOZDploBC0GdyURQQprwo6jOTUUQ8spXgRQXtK4xE= code.fbi.h-da.de/danet/api v0.2.5-0.20220301081709-d68d321135a7/go.mod h1:J1wwKAHhP3HprrzoNs6f5C56znzvns69FU56oItc3kc= +code.fbi.h-da.de/danet/api v0.2.5-0.20220308110152-3bad30e00536 h1:Yi+0ZiROQ0GG7vbsx7jiGy1pimPw77SdNS+8unZpG30= +code.fbi.h-da.de/danet/api v0.2.5-0.20220308110152-3bad30e00536/go.mod h1:J1wwKAHhP3HprrzoNs6f5C56znzvns69FU56oItc3kc= +code.fbi.h-da.de/danet/api v0.2.5-0.20220309155741-8041238ad200 h1:ke5wQPnSg78OpMCBpdwPIW91dwKLBwS+K5rrBPGTagU= +code.fbi.h-da.de/danet/api v0.2.5-0.20220309155741-8041238ad200/go.mod h1:J1wwKAHhP3HprrzoNs6f5C56znzvns69FU56oItc3kc= code.fbi.h-da.de/danet/forks/goarista v0.0.0-20210709163519-47ee8958ef40 h1:x7rVYGqfJSMWuYBp+JE6JVMcFP03Gx0mnR2ftsgqjVI= code.fbi.h-da.de/danet/forks/goarista v0.0.0-20210709163519-47ee8958ef40/go.mod h1:uVe3gCeF2DcIho8K9CIO46uAkHW/lUF+fAaUX1vHrF0= code.fbi.h-da.de/danet/forks/google v0.0.0-20210709163519-47ee8958ef40 h1:B45k5tGEdjjdsKK4f+0dQoyReFmsWdwYEzHofA7DPM8= diff --git a/interfaces/plugin/plugin.go b/interfaces/plugin/plugin.go new file mode 100644 index 0000000000000000000000000000000000000000..eb1541ace1eb79e378b3beadcdc6bbc36b9aa87a --- /dev/null +++ b/interfaces/plugin/plugin.go @@ -0,0 +1,106 @@ +package plugin + +import ( + "fmt" + "io/ioutil" + "regexp" + + "code.fbi.h-da.de/danet/gosdn/nucleus/errors" + "github.com/google/uuid" + "gopkg.in/yaml.v3" +) + +// State represents the current state of a plugin within the controller. Since +// the plugins used within the controller are basic go plugins, they can be +// CREATED, BUILT, LOADED or FAULTY. A plugin can not be unloaded (this is a +// limitation of go plugins in general). +type State int64 + +const ( + //CREATED state describes a plugin which has been created but is not yet + //built. + CREATED State = iota + // BUILT state describes a plugin which is built and can be loaded into the + // controller. + BUILT + // LOADED state describes a plugin which is running within the controller. + LOADED + // FAULTY state describes a plugin which couldn't be built or loaded. + FAULTY +) + +// Plugin describes an interface for a plugin within the controller - a plugin +// in the context of the controller is a basic go plugin. A plugin satisfies +// the Storable interface and can be stored. +// +// Note(mbauch): Currently a plugin is built through the controller itself. +// This is fine for the time being, but should be reconsidered for the time to +// come. In the future we should provide a build environment that allows to +// build plugins within the same environment as the controller itself. +type Plugin interface { + ID() uuid.UUID + State() State + Path() string + Manifest() *Manifest + Update() error +} + +// Manifest represents the manifest of a plugin. +type Manifest struct { + // Name of the plugin + Name string `yaml:"name"` + // Author of the plugin + Author string `yaml:"author"` + // Version of the plugin + Version string `yaml:"version"` +} + +// Validate is a method to check if the manifest is valid and is compliant with +// the requirements. +func (m *Manifest) Validate() error { + errs := []error{} + if m.Name == "" { + errs = append(errs, fmt.Errorf("Name is required")) + } + if m.Author == "" { + errs = append(errs, fmt.Errorf("Author is required")) + } + if m.Version == "" { + errs = append(errs, fmt.Errorf("Version is required")) + } + // regex from: https://stackoverflow.com/a/68921827 + validVersion, err := regexp.MatchString(`^v([1-9]\d*|0)(\.(([1-9]\d*)|0)){2}$`, + m.Version) + if err != nil { + errs = append(errs, err) + } + if !validVersion { + errs = append(errs, fmt.Errorf("Version has to be of form: vX.X.X")) + } + if len(errs) != 0 { + return errors.ErrorList{Errors: errs} + } + return nil +} + +// ReadManifestFromFile reads a manifest file and returns a pointer to a newly +// created Manifest. +func ReadManifestFromFile(path string) (*Manifest, error) { + manifest := &Manifest{} + + manifestFile, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(manifestFile, manifest) + if err != nil { + return nil, err + } + + // validate the loaded manifest + if err := manifest.Validate(); err != nil { + return nil, err + } + + return manifest, nil +} diff --git a/interfaces/southbound/sbi.go b/interfaces/southbound/sbi.go index 0024081c35a14b367adab9e077ad716ba3f8db03..0699b2763a8f7fd1d4966269c1e7a1b693739036 100644 --- a/interfaces/southbound/sbi.go +++ b/interfaces/southbound/sbi.go @@ -13,15 +13,13 @@ import ( // SouthboundInterface provides an // interface for SBI implementations type SouthboundInterface interface { // nolint - // deprecated - SbiIdentifier() string - // SetNode injects SBI specific model // representation to the transport. // Needed for type assertion. SetNode(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error Schema() *ytypes.Schema ID() uuid.UUID + SetID(id uuid.UUID) Type() spb.Type Unmarshal([]byte, *gpb.Path, ygot.ValidatedGoStruct, ...ytypes.UnmarshalOpt) error } diff --git a/mocks/Csbi.go b/mocks/Csbi.go new file mode 100644 index 0000000000000000000000000000000000000000..e87f8d70a58170d018e4fa7aecaee24d9b6dca80 --- /dev/null +++ b/mocks/Csbi.go @@ -0,0 +1,177 @@ +// Code generated by mockery v2.9.4. DO NOT EDIT. +// It says DO NOT EDIT but it has been edited. :-) +// Combines a Plugin and a SouthboundInterface to represent a Csbi. + +package mocks + +import ( + gosdnsouthbound "code.fbi.h-da.de/danet/api/go/gosdn/southbound" + plugin "code.fbi.h-da.de/danet/gosdn/interfaces/plugin" + gnmi "github.com/openconfig/gnmi/proto/gnmi" + + mock "github.com/stretchr/testify/mock" + + uuid "github.com/google/uuid" + + yang "github.com/openconfig/goyang/pkg/yang" + + ygot "github.com/openconfig/ygot/ygot" + + ytypes "github.com/openconfig/ygot/ytypes" +) + +// Csbi is an autogenerated mock type for the Csbi type +type Csbi struct { + mock.Mock +} + +// ID provides a mock function with given fields: +func (_m *Csbi) ID() uuid.UUID { + ret := _m.Called() + + var r0 uuid.UUID + if rf, ok := ret.Get(0).(func() uuid.UUID); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(uuid.UUID) + } + } + + return r0 +} + +// Manifest provides a mock function with given fields: +func (_m *Csbi) Manifest() *plugin.Manifest { + ret := _m.Called() + + var r0 *plugin.Manifest + if rf, ok := ret.Get(0).(func() *plugin.Manifest); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.Manifest) + } + } + + return r0 +} + +// Path provides a mock function with given fields: +func (_m *Csbi) Path() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// State provides a mock function with given fields: +func (_m *Csbi) State() plugin.State { + ret := _m.Called() + + var r0 plugin.State + if rf, ok := ret.Get(0).(func() plugin.State); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(plugin.State) + } + + return r0 +} + +// Update provides a mock function with given fields: +func (_m *Csbi) Update() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Schema provides a mock function with given fields: +func (_m *Csbi) Schema() *ytypes.Schema { + ret := _m.Called() + + var r0 *ytypes.Schema + if rf, ok := ret.Get(0).(func() *ytypes.Schema); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ytypes.Schema) + } + } + + return r0 +} + +// SetID provides a mock function with given fields: id +func (_m *Csbi) SetID(id uuid.UUID) { + _m.Called(id) +} + +// SetNode provides a mock function with given fields: schema, root, path, val, opts +func (_m *Csbi) SetNode(schema *yang.Entry, root interface{}, path *gnmi.Path, val interface{}, opts ...ytypes.SetNodeOpt) error { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, schema, root, path, val) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(*yang.Entry, interface{}, *gnmi.Path, interface{}, ...ytypes.SetNodeOpt) error); ok { + r0 = rf(schema, root, path, val, opts...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Type provides a mock function with given fields: +func (_m *Csbi) Type() gosdnsouthbound.Type { + ret := _m.Called() + + var r0 gosdnsouthbound.Type + if rf, ok := ret.Get(0).(func() gosdnsouthbound.Type); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(gosdnsouthbound.Type) + } + + return r0 +} + +// Unmarshal provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *Csbi) Unmarshal(_a0 []byte, _a1 *gnmi.Path, _a2 ygot.ValidatedGoStruct, _a3 ...ytypes.UnmarshalOpt) error { + _va := make([]interface{}, len(_a3)) + for _i := range _a3 { + _va[_i] = _a3[_i] + } + var _ca []interface{} + _ca = append(_ca, _a0, _a1, _a2) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func([]byte, *gnmi.Path, ygot.ValidatedGoStruct, ...ytypes.UnmarshalOpt) error); ok { + r0 = rf(_a0, _a1, _a2, _a3...) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/GenericGoStructClient.go b/mocks/GenericGoStructClient.go new file mode 100644 index 0000000000000000000000000000000000000000..9aa0aee7ee2c83ca716d32bd1c66c5ff4ab5f6d4 --- /dev/null +++ b/mocks/GenericGoStructClient.go @@ -0,0 +1,137 @@ +// Code generated by mockery v2.9.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + csbi "code.fbi.h-da.de/danet/api/go/gosdn/csbi" + metadata "google.golang.org/grpc/metadata" + + mock "github.com/stretchr/testify/mock" +) + +// GenericGoStructClient is an autogenerated mock type for the GenericGoStructClient type +type GenericGoStructClient struct { + mock.Mock +} + +// CloseSend provides a mock function with given fields: +func (_m *GenericGoStructClient) CloseSend() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Context provides a mock function with given fields: +func (_m *GenericGoStructClient) Context() context.Context { + ret := _m.Called() + + var r0 context.Context + if rf, ok := ret.Get(0).(func() context.Context); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(context.Context) + } + } + + return r0 +} + +// Header provides a mock function with given fields: +func (_m *GenericGoStructClient) Header() (metadata.MD, error) { + ret := _m.Called() + + var r0 metadata.MD + if rf, ok := ret.Get(0).(func() metadata.MD); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(metadata.MD) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Recv provides a mock function with given fields: +func (_m *GenericGoStructClient) Recv() (*csbi.Payload, error) { + ret := _m.Called() + + var r0 *csbi.Payload + if rf, ok := ret.Get(0).(func() *csbi.Payload); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*csbi.Payload) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RecvMsg provides a mock function with given fields: m +func (_m *GenericGoStructClient) RecvMsg(m interface{}) error { + ret := _m.Called(m) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(m) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendMsg provides a mock function with given fields: m +func (_m *GenericGoStructClient) SendMsg(m interface{}) error { + ret := _m.Called(m) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(m) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Trailer provides a mock function with given fields: +func (_m *GenericGoStructClient) Trailer() metadata.MD { + ret := _m.Called() + + var r0 metadata.MD + if rf, ok := ret.Get(0).(func() metadata.MD); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(metadata.MD) + } + } + + return r0 +} diff --git a/mocks/Plugin.go b/mocks/Plugin.go new file mode 100644 index 0000000000000000000000000000000000000000..7fe84bb458744064a0165df164b6048fa3a605ca --- /dev/null +++ b/mocks/Plugin.go @@ -0,0 +1,88 @@ +// Code generated by mockery v2.9.4. DO NOT EDIT. + +package mocks + +import ( + plugin "code.fbi.h-da.de/danet/gosdn/interfaces/plugin" + uuid "github.com/google/uuid" + mock "github.com/stretchr/testify/mock" +) + +// Plugin is an autogenerated mock type for the Plugin type +type Plugin struct { + mock.Mock +} + +// ID provides a mock function with given fields: +func (_m *Plugin) ID() uuid.UUID { + ret := _m.Called() + + var r0 uuid.UUID + if rf, ok := ret.Get(0).(func() uuid.UUID); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(uuid.UUID) + } + } + + return r0 +} + +// Manifest provides a mock function with given fields: +func (_m *Plugin) Manifest() *plugin.Manifest { + ret := _m.Called() + + var r0 *plugin.Manifest + if rf, ok := ret.Get(0).(func() *plugin.Manifest); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.Manifest) + } + } + + return r0 +} + +// Path provides a mock function with given fields: +func (_m *Plugin) Path() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// State provides a mock function with given fields: +func (_m *Plugin) State() plugin.State { + ret := _m.Called() + + var r0 plugin.State + if rf, ok := ret.Get(0).(func() plugin.State); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(plugin.State) + } + + return r0 +} + +// Update provides a mock function with given fields: +func (_m *Plugin) Update() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/SouthboundInterface.go b/mocks/SouthboundInterface.go index 22dd94e9ef59d87afb370f2a09f2fa4642b53de9..12bd7100f01ea6c1df65833f3bcd9fe50ff791b6 100644 --- a/mocks/SouthboundInterface.go +++ b/mocks/SouthboundInterface.go @@ -38,20 +38,6 @@ func (_m *SouthboundInterface) ID() uuid.UUID { return r0 } -// SbiIdentifier provides a mock function with given fields: -func (_m *SouthboundInterface) SbiIdentifier() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - // Schema provides a mock function with given fields: func (_m *SouthboundInterface) Schema() *ytypes.Schema { ret := _m.Called() @@ -68,6 +54,11 @@ func (_m *SouthboundInterface) Schema() *ytypes.Schema { return r0 } +// SetID provides a mock function with given fields: id +func (_m *SouthboundInterface) SetID(id uuid.UUID) { + _m.Called(id) +} + // SetNode provides a mock function with given fields: schema, root, path, val, opts func (_m *SouthboundInterface) SetNode(schema *yang.Entry, root interface{}, path *gnmi.Path, val interface{}, opts ...ytypes.SetNodeOpt) error { _va := make([]interface{}, len(opts)) diff --git a/northbound/server/core.go b/northbound/server/core.go index d269fad776b087d52769f993a83328e7ee193329..65ba3b82c42ffda30872438a244958b29e237659 100644 --- a/northbound/server/core.go +++ b/northbound/server/core.go @@ -6,7 +6,6 @@ import ( pb "code.fbi.h-da.de/danet/api/go/gosdn/core" ppb "code.fbi.h-da.de/danet/api/go/gosdn/pnd" - spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound" "code.fbi.h-da.de/danet/gosdn/metrics" "code.fbi.h-da.de/danet/gosdn/nucleus" "github.com/google/uuid" @@ -77,12 +76,7 @@ func (s core) CreatePndList(ctx context.Context, request *pb.CreatePndListReques start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) for _, r := range request.Pnd { - sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) - if err != nil { - return nil, handleRPCError(labels, err) - } - - pnd, err := nucleus.NewPND(r.Name, r.Description, uuid.New(), sbi, nil, nil) + pnd, err := nucleus.NewPND(r.Name, r.Description, uuid.New(), nil, nil, nil) if err != nil { return nil, handleRPCError(labels, err) } diff --git a/northbound/server/pnd_test.go b/northbound/server/pnd_test.go index b2e7595ab319184f92ed2fdf07a414e9150bbe1f..9eedf11338595a0efcfbc5feeae003f6d6adb8ca 100644 --- a/northbound/server/pnd_test.go +++ b/northbound/server/pnd_test.go @@ -128,14 +128,15 @@ func TestMain(m *testing.M) { }, UUID: deviceUUID, } - sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) + + sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG, sbiUUID) if err != nil { log.Fatal(err) } mockDevice.(*nucleus.CommonDevice).SetSBI(sbi) mockDevice.(*nucleus.CommonDevice).SetTransport(&mocks.Transport{}) mockDevice.(*nucleus.CommonDevice).SetName(hostname) - sbiStore = store.NewSbiStore() + sbiStore = store.NewSbiStore(pndUUID) if err := sbiStore.Add(mockDevice.SBI()); err != nil { log.Fatal(err) } diff --git a/nucleus/device.go b/nucleus/device.go index 8ded445919e525849b7280431555cf23a852ae79..464c9fd89b6561f90ae820fa6421199212c29f6a 100644 --- a/nucleus/device.go +++ b/nucleus/device.go @@ -21,7 +21,8 @@ func NewDevice(name string, uuidInput uuid.UUID, opt *tpb.TransportOption, sbi s return nil, err } - // TODO: this needs to check the case that the uuidInput is set, as the same uuid may be already stored. + // TODO: this needs to check the case that the uuidInput is set, as the + // same uuid may be already stored. if uuidInput == uuid.Nil { uuidInput = uuid.New() } diff --git a/nucleus/errors/errors.go b/nucleus/errors/errors.go index 7654c6ec36a56a6270f8a0431399f528a67ead6c..2e75d8a5da928457b8c03f8107172359b6509193 100644 --- a/nucleus/errors/errors.go +++ b/nucleus/errors/errors.go @@ -110,6 +110,42 @@ func (e ErrOperationNotSupported) Error() string { return fmt.Sprintf("transport operation not supported: %v", reflect.TypeOf(e.Op)) } +// ErrUnsupportedSbiType implements the Error interface and is called if the +// wrong Type for a SBI has been provided. +type ErrUnsupportedSbiType struct { + Type interface{} +} + +func (e ErrUnsupportedSbiType) Error() string { + return fmt.Sprintf("SBI type not supported: %v", e.Type) +} + +// ErrPluginVersion implements the Error interface and is called if the Version +// of a Plugin is older than a Plugin in use. +type ErrPluginVersion struct { + PlugID, ProvidedVer, UsedVer string +} + +func (e ErrPluginVersion) Error() string { + return fmt.Sprintf("Version of Plugin: %s is older than the one in use. Provided: %s, in use: %s", e.PlugID, e.ProvidedVer, e.UsedVer) +} + +// ErrorList implements the Error interface and is called if a slice of errors +// should be returned. The slice of errors is combined into a single error +// message and returned. +type ErrorList struct { + Errors []error +} + +func (e ErrorList) Error() string { + combinedErrString := "Errors found:" + for i, err := range e.Errors { + errString := fmt.Sprintf("\n %v. %v", i+1, err) + combinedErrString = combinedErrString + errString + } + return combinedErrString +} + // ErrTypeNotSupported implements the Error interface and is called if the // wrong Type has been provided. type ErrTypeNotSupported struct { diff --git a/nucleus/gnmi_transport_test.go b/nucleus/gnmi_transport_test.go index b7178956eee5763affb1e415029d5e1279e1ea07..040ee2cc3d9699d24d28c0e1325053b7988a79f5 100644 --- a/nucleus/gnmi_transport_test.go +++ b/nucleus/gnmi_transport_test.go @@ -459,11 +459,13 @@ func TestNewGnmiTransport(t *testing.T) { if tt.name == "default" { startGnmiTarget <- gnmiConfig.Addr } - oc, err := NewSBI(spb.Type_TYPE_OPENCONFIG) + + sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("NewSBI() error = %v", err) + return } - got, err := newGnmiTransport(tt.args.opts, oc) + got, err := newGnmiTransport(tt.args.opts, sbi) if (err != nil) != tt.wantErr { t.Errorf("NewGnmiTransport() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/nucleus/initialise_test.go b/nucleus/initialise_test.go index f0d9b10981d71eb2b8b6c0617aa6809b7c064c17..438dfcaba0a920787bc7f102be49f2f16efedf26 100644 --- a/nucleus/initialise_test.go +++ b/nucleus/initialise_test.go @@ -152,7 +152,7 @@ func newPnd() pndImplementation { return pndImplementation{ Name: "default", Description: "default test pnd", - sbic: store.NewSbiStore(), + sbic: store.NewSbiStore(defaultPndID), devices: store.NewDeviceStore(defaultPndID), changes: store.NewChangeStore(), Id: defaultPndID, diff --git a/nucleus/plugin.go b/nucleus/plugin.go new file mode 100644 index 0000000000000000000000000000000000000000..05f19b8096657bd848e0e7c32af2a4f744dbd848 --- /dev/null +++ b/nucleus/plugin.go @@ -0,0 +1,91 @@ +package nucleus + +import ( + "bytes" + "os/exec" + "path/filepath" + goPlugin "plugin" + + "code.fbi.h-da.de/danet/gosdn/interfaces/plugin" + "code.fbi.h-da.de/danet/gosdn/nucleus/errors" + log "github.com/sirupsen/logrus" +) + +// BuildPlugin builds a new plugin within the given path. The source file must +// be present at the given path. The built plugin is written to a 'plugin.so' +// file. +func BuildPlugin(path, sourceFileName string, args ...string) error { + pPath := filepath.Join(path, "plugin.so") + sPath := filepath.Join(path, sourceFileName) + // Provide standard build arguments within a string slice + buildCommand := []string{ + "go", + "build", + "-o", + pPath, + "-buildmode=plugin", + } + // Append build arguments + buildCommand = append(buildCommand, args...) + buildCommand = append(buildCommand, sPath) + + var stderr bytes.Buffer + // Create the command to be executed + cmd := exec.Command(buildCommand[0], buildCommand[1:]...) + cmd.Dir = "./" + cmd.Stderr = &stderr + // Run the command and build the plugin + err := cmd.Run() + if err != nil { + log.Error(stderr.String()) + return err + } + return nil +} + +// LoadPlugin opens a go plugin binary which must be named 'plugin.so' at the +// given path. LoadPlugin checks for the symbol 'PluginSymbol' that has to be +// provided within the plugin to be loaded. If the symbol is found, the loaded +// plugin is returned. +func LoadPlugin(path string) (goPlugin.Symbol, error) { + path = filepath.Join(path, "plugin.so") + // Open the plugin, which is a binary (named 'plugin.so') within the given + // path. + gp, err := goPlugin.Open(path) + if err != nil { + return nil, err + } + // Search for the specific symbol within the plugin. If it does not contain + // the symbol is not usable and an error is thrown. + symbol, err := gp.Lookup("PluginSymbol") + if err != nil { + return nil, err + } + + return symbol, nil +} + +// UpdatePlugin updates a given Plugin. Therefore the version of the +// `plugin.yml` manifest file is compared to the version in use. If a new +// version is within the plugin folder, the new version of the plugin is built. +// NOTE:This should only be used with caution. +func UpdatePlugin(p plugin.Plugin) (updated bool, err error) { + tmpManifest, err := plugin.ReadManifestFromFile(filepath.Join(p.Path(), "plugin.yml")) + if err != nil { + return false, err + } + + if p.Manifest().Version < tmpManifest.Version { + err := BuildPlugin(p.Path(), "gostructs.go") + if err != nil { + return false, err + } + log.Info("Plugin update executed.") + return true, nil + } + return false, errors.ErrPluginVersion{ + PlugID: p.ID().String(), + ProvidedVer: tmpManifest.Version, + UsedVer: p.Manifest().Version, + } +} diff --git a/nucleus/plugin_test.go b/nucleus/plugin_test.go new file mode 100644 index 0000000000000000000000000000000000000000..daeeae3a27d1c4f86521a1750ec578fbd3437bf5 --- /dev/null +++ b/nucleus/plugin_test.go @@ -0,0 +1,243 @@ +package nucleus + +import ( + "crypto/sha256" + "encoding/hex" + "io" + "os" + "path/filepath" + "testing" + + "code.fbi.h-da.de/danet/gosdn/interfaces/plugin" + "code.fbi.h-da.de/danet/gosdn/interfaces/southbound" + "code.fbi.h-da.de/danet/gosdn/mocks" + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" +) + +// Helper function to generate hash value of a file. +func generateHash(path, fileName string) (string, error) { + fp := filepath.Join(path, fileName) + f, err := os.Open(fp) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return hex.EncodeToString(h.Sum(nil)), nil +} + +func Test_plugin_BuildPlugin(t *testing.T) { + type args struct { + path, goStructName, pluginName string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "build success", + args: args{ + path: "../test/plugin", + goStructName: "gostructs.go", + pluginName: "plugin.so", + }, + // hacky, but if run locally use: + // a5332eff85bfab88cd6ccba38c3c8f7137d00d7cc1cf7da81666b3af4829d330 + want: "ab36591e1af5ef34bca12927a58e2131954a5ae5ae3f13e02954ae6a0b89e88e", + wantErr: false, + }, + { + name: "fail: file does not exist", + args: args{ + path: "../test/plugin", + goStructName: "doesNotExist.go", + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := BuildPlugin(tt.args.path, tt.args.goStructName, "-race") + if (err != nil) != tt.wantErr { + t.Errorf("BuildPlugin() error = %v, wantErr %v", err, tt.wantErr) + return + } + + hash, err := generateHash(tt.args.path, tt.args.pluginName) + if err != nil { + if tt.name != "fail: file does not exist" { + t.Errorf("BuildPlugin() error = %v", err) + return + } + } + if !cmp.Equal(hash, tt.want) { + t.Errorf("BuildPlugin() got = %v, want %v", hash, tt.want) + } + }) + } +} + +func Test_plugin_LoadPlugin(t *testing.T) { + type args struct { + path string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "load success", + args: args{ + path: "../test/plugin", + }, + wantErr: false, + }, + { + name: "symbol not provided", + args: args{ + path: "../test/plugin/faulty", + }, + wantErr: true, + }, + { + name: "fail: wrong path", + args: args{ + path: "../test/plugin/wrong/path", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name != "fail: wrong path" { + if err := BuildPlugin(tt.args.path, "gostructs.go", "-race"); err != nil { + t.Errorf("LoadPlugin() error = %v", err) + return + } + } + + ps, err := LoadPlugin(tt.args.path) + if (err != nil) != tt.wantErr { + t.Errorf("LoadPlugin() error = %v, wantErr %v", err, tt.wantErr) + return + } + // the plugin provided for test cases should be assertable to type + // southbound.SouthboundInterface + if tt.name == "load success" { + if _, ok := ps.(southbound.SouthboundInterface); !ok { + t.Error("LoadPlugin() error = plugin.Symbol was not assertable to southbound.SouthboundInterface") + } + } + }) + } +} + +func Test_plugin_UpdatePlugin(t *testing.T) { + newPlugin := func(id uuid.UUID, path string, state plugin.State, manifest *plugin.Manifest) *mocks.Plugin { + p := &mocks.Plugin{} + p.On("Path").Return(path) + p.On("ID").Return(id) + p.On("State").Return(state) + p.On("Manifest").Return(manifest) + return p + } + + type args struct { + id uuid.UUID + path string + state plugin.State + manifest *plugin.Manifest + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "Update success", + args: args{ + id: uuid.New(), + path: "../test/plugin", + state: plugin.State(2), + manifest: &plugin.Manifest{ + Name: "test", + Author: "goSDN-Team", + Version: "v1.0.2", + }, + }, + want: true, + wantErr: false, + }, + { + name: "Plugin to Update is of older version", + args: args{ + id: uuid.New(), + path: "../test/plugin", + state: plugin.State(2), + manifest: &plugin.Manifest{ + Name: "test", + Author: "goSDN-Team", + Version: "v2.2.2", + }, + }, + want: false, + wantErr: true, + }, + { + name: "Faulty manifest file", + args: args{ + id: uuid.New(), + path: "../test/plugin/faulty", + state: plugin.State(2), + manifest: &plugin.Manifest{ + Name: "test", + Author: "goSDN-Team", + Version: "v1.0.0", + }, + }, + want: false, + wantErr: true, + }, + { + name: "wrong path", + args: args{ + id: uuid.New(), + path: "../test/plugin/wrong/path", + state: plugin.State(2), + manifest: &plugin.Manifest{ + Name: "test", + Author: "goSDN-Team", + Version: "v1.0.2", + }, + }, + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := newPlugin(tt.args.id, tt.args.path, tt.args.state, tt.args.manifest) + + got, err := UpdatePlugin(p) + if (err != nil) != tt.wantErr { + t.Errorf("UpdatePlugin() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !cmp.Equal(got, tt.want) { + t.Errorf("BuildPlugin() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/nucleus/principalNetworkDomain.go b/nucleus/principalNetworkDomain.go index 49792756d32d88a53dba0dfbe8eda90b22e40a6b..9e92cd43d873158b5e457a83da95e76e0884371e 100644 --- a/nucleus/principalNetworkDomain.go +++ b/nucleus/principalNetworkDomain.go @@ -5,7 +5,7 @@ import ( "encoding/json" "io" "os" - "plugin" + "path/filepath" "time" "code.fbi.h-da.de/danet/gosdn/metrics" @@ -16,6 +16,7 @@ import ( ppb "code.fbi.h-da.de/danet/api/go/gosdn/pnd" spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound" tpb "code.fbi.h-da.de/danet/api/go/gosdn/transport" + "google.golang.org/grpc" "google.golang.org/protobuf/proto" "code.fbi.h-da.de/danet/forks/goarista/gnmi" @@ -33,6 +34,7 @@ import ( "github.com/openconfig/ygot/ytypes" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" + "github.com/spf13/viper" ) // NewPND creates a Principle Network Domain @@ -40,7 +42,7 @@ func NewPND(name, description string, id uuid.UUID, sbi southbound.SouthboundInt pnd := &pndImplementation{ Name: name, Description: description, - sbic: store.NewSbiStore(), + sbic: store.NewSbiStore(id), devices: store.NewDeviceStore(id), changes: store.NewChangeStore(), Id: id, @@ -49,10 +51,19 @@ func NewPND(name, description string, id uuid.UUID, sbi southbound.SouthboundInt callback: callback, } - if err := pnd.sbic.Add(sbi); err != nil { + if err := pnd.loadStoredSbis(); err != nil { return nil, err } + // If the SBI is not provided, then do not add a SBI to the store + if sbi != nil { + if !pnd.sbic.Exists(sbi.ID()) { + if err := pnd.sbic.Add(sbi); err != nil { + return nil, err + } + } + } + if err := pnd.loadStoredDevices(); err != nil { return nil, err } @@ -143,7 +154,30 @@ func (pnd *pndImplementation) AddSbi(s southbound.SouthboundInterface) error { return pnd.addSbi(s) } -// RemoveSbi removes a SBI and all the associated devices from the PND +// AddSbiFromStore creates a SBI based on the given ID, type and path provided. +// The type determines if a SouthboundPlugin or a standard OpenConfig SBI is +// created. The SBI is then added to the PND's SBI store. +func (pnd *pndImplementation) AddSbiFromStore(id uuid.UUID, sbiType string, path string) error { + var sbi southbound.SouthboundInterface + var err error + if spb.Type_value[sbiType] != int32(spb.Type_TYPE_OPENCONFIG) { + sbi, err = NewSouthboundPlugin(id, path, false) + if err != nil { + return err + } + } else { + sbi, err = NewSBI(spb.Type_TYPE_OPENCONFIG, id) + if err != nil { + return err + } + } + return pnd.addSbi(sbi) +} + +// RemoveSbi removes a SBI from the PND +// TODO: this should to recursively through +// devices and remove the devices using +// this SBI func (pnd *pndImplementation) RemoveSbi(id uuid.UUID) error { associatedDevices, err := pnd.devices.GetDevicesAssociatedWithSbi(id) if err != nil { @@ -227,20 +261,22 @@ func (pnd *pndImplementation) RemoveDevice(uuid uuid.UUID) error { return pnd.removeDevice(uuid) } -// Actual implementation, bind to struct if -// neccessary +// Actual implementation, bind to struct if neccessary func destroy() error { return nil } +// addSbi adds a SBI to the PND's SBI store. func (pnd *pndImplementation) addSbi(sbi southbound.SouthboundInterface) error { return pnd.sbic.Add(sbi) } +// removeSbi removes an SBI based on the given ID from the PND's SBI store. func (pnd *pndImplementation) removeSbi(id uuid.UUID) error { return pnd.sbic.Delete(id) } +// addDevice adds a device to the PND's device store. func (pnd *pndImplementation) addDevice(device device.Device) error { err := pnd.devices.Add(device, device.Name()) if err != nil { @@ -255,7 +291,7 @@ func (pnd *pndImplementation) removeDevice(id uuid.UUID) error { if err != nil { return err } - labels := prometheus.Labels{"type": d.SBI().SbiIdentifier()} + labels := prometheus.Labels{"type": d.SBI().Type().String()} start := metrics.StartHook(labels, deviceDeletionsTotal) defer metrics.FinishHook(labels, start, deviceDeletionDurationSecondsTotal, deviceDeletionDurationSeconds) switch d.(type) { @@ -462,6 +498,38 @@ func (pnd *pndImplementation) createCsbiDevice(ctx context.Context, name string, TransportOption: opt.TransportOption, } log.WithField("transport option", csbiTransportOptions).Debug("gosdn gnmi transport options") + req := &cpb.GetRequest{ + Timestamp: time.Now().UnixNano(), + All: false, + Did: []string{d.Id}, + } + idd := uuid.New() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) + defer cancel() + gClient, err := pnd.csbiClient.GetGoStruct(ctx, req) + if err != nil { + return err + } + csbiID, err := saveGenericClientStreamToFile(gClient, "gostructs.go", idd) + if err != nil { + return err + } + mClient, err := pnd.csbiClient.GetManifest(ctx, req) + if err != nil { + return err + } + _, err = saveGenericClientStreamToFile(mClient, "plugin.yml", idd) + if err != nil { + return err + } + csbi, err := NewSBI(spb.Type_TYPE_CONTAINERISED, csbiID) + if err != nil { + return err + } + err = pnd.sbic.Add(csbi) + if err != nil { + return err + } d, err := NewDevice(name, uuid.Nil, csbiTransportOptions, csbi) if err != nil { return err @@ -472,78 +540,111 @@ func (pnd *pndImplementation) createCsbiDevice(ctx context.Context, name string, return err } } - return nil } +// requestPlugin is a feature for cSBIs and sends a plugin request to the cSBI +// orchestrator and processes the received ygot generated go code, builds the +// plugin and integrates the Plugin within the goSDN as SouthboundInterface. +// The generated code is passed into a gostructs.go file, which is the +// foundation for the created plugin. func (pnd *pndImplementation) requestPlugin(name string, opt *tpb.TransportOption) (southbound.SouthboundInterface, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) defer cancel() - req := &cpb.CreatePluginRequest{ + req := &cpb.CreateRequest{ Timestamp: time.Now().UnixNano(), TransportOption: []*tpb.TransportOption{opt}, } - client, err := pnd.csbiClient.CreatePlugin(ctx, req) + client, err := pnd.csbiClient.CreateGoStruct(ctx, req) if err != nil { return nil, err } - id := uuid.New() - f, err := os.Create("plugin-" + id.String() + ".so") + id, err := saveGenericClientStreamToFile(client, "gostructs.go", uuid.New()) if err != nil { return nil, err } + + sbi, err := NewSBI(spb.Type_TYPE_PLUGIN, id) + if err != nil { + return nil, err + } + err = pnd.sbic.Add(sbi) + if err != nil { + return nil, err + } + return sbi, nil +} + +// GenericGrpcClient allows to distinguish between the different ygot +// generated GoStruct clients, which return a stream of bytes. +type GenericGrpcClient interface { + Recv() (*cpb.Payload, error) + grpc.ClientStream +} + +// saveGenericClientStreamToFile takes a GenericGoStructClient and processes the included +// gRPC stream. A 'gostructs.go' file is created within the goSDN's +// 'plugin-folder'. Each 'gostructs.go' file is stored in its own folder based +// on a new uuid.UUID. +func saveGenericClientStreamToFile(t GenericGrpcClient, filename string, id uuid.UUID) (uuid.UUID, error) { + //id := uuid.New() + folderName := viper.GetString("plugin-folder") + path := filepath.Join(folderName, id.String(), filename) + + // create the directory hierarchy based on the path + if err := os.MkdirAll(filepath.Dir(path), 0770); err != nil { + return uuid.Nil, err + } + // create the gostructs.go file at path + f, err := os.Create(path) + if err != nil { + return uuid.Nil, err + } defer f.Close() + + // receive byte stream for { - payload, err := client.Recv() + payload, err := t.Recv() if err != nil { if err == io.EOF { break } - client.CloseSend() - return nil, err + t.CloseSend() + return uuid.Nil, err } n, err := f.Write(payload.Chunk) if err != nil { - client.CloseSend() - return nil, err + t.CloseSend() + return uuid.Nil, err } log.WithField("n", n).Trace("wrote bytes") } if err := f.Sync(); err != nil { - return nil, err + return uuid.Nil, err } - return loadPlugin(id) + return id, nil } -func loadPlugin(id uuid.UUID) (southbound.SouthboundInterface, error) { - p, err := plugin.Open("plugin-" + id.String() + ".so") +// loadStoredSbis loads all stored SBIs and add each one of them to the PND's +// SBI store. +func (pnd *pndImplementation) loadStoredSbis() error { + sbis, err := pnd.sbic.Load() if err != nil { - return nil, err - } - - symbol, err := p.Lookup("PluginSymbol") - if err != nil { - return nil, err + return err } - - var sbi southbound.SouthboundInterface - sbi, ok := symbol.(southbound.SouthboundInterface) - if !ok { - return nil, &errors.ErrInvalidTypeAssertion{ - Value: symbol, - Type: (*southbound.SouthboundInterface)(nil), + for _, sbi := range sbis { + err := pnd.AddSbiFromStore(sbi.ID, sbi.Type, sbi.Path) + if err != nil { + return err } } - log.WithFields(log.Fields{ - "identifier": sbi.SbiIdentifier(), - "id": sbi.ID(), - "type": sbi.Type(), - }).Trace("plugin information") - return sbi, nil + return nil } +// loadStoredDevices loads all stored devices and adds each one of them to the +// PND's device store. func (pnd *pndImplementation) loadStoredDevices() error { devices, err := pnd.devices.Load() if err != nil { diff --git a/nucleus/principalNetworkDomain_test.go b/nucleus/principalNetworkDomain_test.go index 71670294ee1167612ccdef5b34c437795c00d20a..2622c91a0ab6baaa90937c8381d8698cda1e22af 100644 --- a/nucleus/principalNetworkDomain_test.go +++ b/nucleus/principalNetworkDomain_test.go @@ -3,10 +3,12 @@ package nucleus import ( "errors" "fmt" + "io" "os" "reflect" "testing" + "code.fbi.h-da.de/danet/api/go/gosdn/csbi" ppb "code.fbi.h-da.de/danet/api/go/gosdn/pnd" spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound" tpb "code.fbi.h-da.de/danet/api/go/gosdn/transport" @@ -199,6 +201,58 @@ func Test_pndImplementation_AddSbi(t *testing.T) { } } +func Test_pndImplementation_AddSbiFromStore(t *testing.T) { + type args struct { + uuid uuid.UUID + ttype spb.Type + sbi southbound.SouthboundInterface + path string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default", + args: args{ + uuid: defaultSbiID, + ttype: spb.Type_TYPE_OPENCONFIG, + path: "", + }, + wantErr: false, + }, + { + name: "already exists", + args: args{ + uuid: defaultSbiID, + ttype: spb.Type_TYPE_OPENCONFIG, + path: "", + sbi: &OpenConfig{ + id: defaultSbiID, + schema: nil, + path: "", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pnd := newPnd() + + if tt.name == "already exists" { + pnd.sbic.Store[defaultSbiID] = tt.args.sbi.(southbound.SouthboundInterface) + } + + err := pnd.AddSbiFromStore(tt.args.uuid, tt.args.ttype.String(), tt.args.path) + if (err != nil) != tt.wantErr { + t.Errorf("AddSbiFromStore() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + func Test_pndImplementation_ContainsDevice(t *testing.T) { removeExistingPNDStore() @@ -373,11 +427,6 @@ func Test_pndImplementation_MarshalDevice(t *testing.T) { func Test_pndImplementation_RemoveDevice(t *testing.T) { removeExistingPNDStore() - sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) - if err != nil { - t.Errorf("NewSBI() error = %v", err) - } - type args struct { uuid uuid.UUID } @@ -395,6 +444,11 @@ func Test_pndImplementation_RemoveDevice(t *testing.T) { 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, @@ -437,7 +491,7 @@ func Test_pndImplementation_RemoveSbi(t *testing.T) { pnd := &pndImplementation{ Name: "test-remove-sbi", Description: "test-remove-sbi", - sbic: store.NewSbiStore(), + sbic: store.NewSbiStore(defaultPndID), devices: store.NewDeviceStore(defaultPndID), Id: defaultPndID, } @@ -715,8 +769,8 @@ func Test_pndImplementation_GetDevice(t *testing.T) { sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("NewSBI() error = %v", err) + return } - d, err := NewDevice("", uuid.Nil, newGnmiTransportOptions(), sbi) if err != nil { t.Error(err) @@ -769,8 +823,8 @@ func Test_pndImplementation_GetDeviceByName(t *testing.T) { 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) if err != nil { t.Error(err) @@ -1038,6 +1092,8 @@ func Test_pndImplementation_LoadStoredDevices(t *testing.T) { } } +// TODO(mbauch): This test case looks unfinished. For example there are no +// cases for 'already exists' and 'fails wrong type'. func Test_pndImplementation_AddDeviceWithUUID(t *testing.T) { type args struct { uuid uuid.UUID @@ -1099,3 +1155,92 @@ func Test_pndImplementation_AddDeviceWithUUID(t *testing.T) { }) } } + +func Test_pndImplementation_saveGoStructsToFile(t *testing.T) { + type genericGoStructClientArg struct { + fn string + rtrn []interface{} + times int + } + // Create a new mock for GenericGoStructClient. With + // genericGoStructClientArg it is possible to set the Return values of the + // mocks methods. + newGenericGoStructClient := func(args ...genericGoStructClientArg) *mocks.GenericGoStructClient { + ggsc := &mocks.GenericGoStructClient{} + for _, arg := range args { + ggsc.On(arg.fn).Return(arg.rtrn...).Times(arg.times) + } + ggsc.On("CloseSend").Return(nil) + return ggsc + } + + type args struct { + id uuid.UUID + client GenericGrpcClient + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default", + args: args{ + id: uuid.New(), + client: newGenericGoStructClient( + []genericGoStructClientArg{ + { + fn: "Recv", + rtrn: []interface{}{ + &csbi.Payload{Chunk: []byte("test")}, + nil, + }, + times: 3, + }, + { + fn: "Recv", + rtrn: []interface{}{ + &csbi.Payload{Chunk: nil}, + io.EOF, + }, + times: 1, + }, + }...), + }, + wantErr: false, + }, + { + name: "unexpected EOF error", + args: args{ + id: uuid.New(), + client: newGenericGoStructClient( + []genericGoStructClientArg{ + { + fn: "Recv", + rtrn: []interface{}{ + &csbi.Payload{Chunk: nil}, + io.ErrUnexpectedEOF, + }, + times: 1, + }, + { + fn: "CloseSend", + rtrn: []interface{}{ + nil, + }, + times: 1, + }, + }...), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := saveGenericClientStreamToFile(tt.args.client, "gostructs.go", tt.args.id) + if (err != nil) != tt.wantErr { + t.Errorf("saveGoStructsToFile() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/nucleus/southbound.go b/nucleus/southbound.go index 4bf8e06ec0a7d2f73bc79d8cd6fe6a689dde6ff1..78ec76b90c886c4b5c215270ba973c67875986eb 100644 --- a/nucleus/southbound.go +++ b/nucleus/southbound.go @@ -1,9 +1,13 @@ package nucleus import ( + "path/filepath" + "reflect" + "code.fbi.h-da.de/danet/gosdn/nucleus/errors" spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound" + "code.fbi.h-da.de/danet/gosdn/interfaces/plugin" "code.fbi.h-da.de/danet/gosdn/interfaces/southbound" "code.fbi.h-da.de/danet/yang-models/generated/openconfig" @@ -13,14 +17,9 @@ import ( "github.com/openconfig/ygot/ygot" "github.com/openconfig/ygot/ytypes" log "github.com/sirupsen/logrus" + "github.com/spf13/viper" ) -var csbi *Csbi - -func init() { - csbi = &Csbi{id: uuid.New()} -} - // NewSBI creates a SouthboundInterface of a given type. func NewSBI(southbound spb.Type, sbUUID ...uuid.UUID) (southbound.SouthboundInterface, error) { var id uuid.UUID @@ -34,9 +33,41 @@ func NewSBI(southbound spb.Type, sbUUID ...uuid.UUID) (southbound.SouthboundInte switch southbound { case spb.Type_TYPE_OPENCONFIG: return &OpenConfig{id: id}, nil + case spb.Type_TYPE_CONTAINERISED, spb.Type_TYPE_PLUGIN: + p := filepath.Join(viper.GetString("plugin-folder"), id.String()) + sbip, err := NewSouthboundPlugin(id, p, true) + if err != nil { + return nil, err + } + return sbip, nil default: - return nil, errors.ErrTypeNotSupported{Type: southbound} + return nil, errors.ErrUnsupportedSbiType{Type: southbound} + } +} + +// NewSouthboundPlugin that returns a new SouthboundPlugin. The plugin is built +// within this process and loaded as southbound.SouthboundInterface afterwards. +func NewSouthboundPlugin(id uuid.UUID, path string, build bool) (*SouthboundPlugin, error) { + manifest, err := plugin.ReadManifestFromFile(filepath.Join(path, "plugin.yml")) + if err != nil { + return nil, err + } + sp := &SouthboundPlugin{ + state: plugin.CREATED, + BinaryPath: path, + manifest: manifest, + sbi: nil, + } + if build { + if err := BuildPlugin(sp.Path(), "gostructs.go"); err != nil { + return nil, err + } + } + if err := sp.load(id); err != nil { + return nil, err } + + return sp, nil } // OpenConfig is the implementation of an OpenConfig SBI. @@ -45,6 +76,7 @@ func NewSBI(southbound spb.Type, sbUUID ...uuid.UUID) (southbound.SouthboundInte type OpenConfig struct { schema *ytypes.Schema id uuid.UUID + path string } // SbiIdentifier returns the string representation of @@ -124,52 +156,118 @@ func (oc *OpenConfig) ID() uuid.UUID { return oc.id } -// Type returns the Southbound's type -func (oc *OpenConfig) Type() spb.Type { return spb.Type_TYPE_OPENCONFIG } +// SetID sets the ID of the OpenConfig SBI +func (oc *OpenConfig) SetID(id uuid.UUID) { + oc.id = id +} -// Csbi is a stub for the containerised SBI functionality. -// It holds the standard goSDN OPENCONFIG schema for minimum -// compatibility -type Csbi struct { - schema *ytypes.Schema - id uuid.UUID +// Type returns the Southbound's type +func (oc *OpenConfig) Type() spb.Type { + return spb.Type_TYPE_OPENCONFIG } -// SbiIdentifier returns the identifier as a -func (csbi *Csbi) SbiIdentifier() string { - return "csbi" +// SouthboundPlugin is the implementation of a southbound goSDN plugin. +type SouthboundPlugin struct { + sbi southbound.SouthboundInterface + state plugin.State + BinaryPath string + manifest *plugin.Manifest } -// SetNode injects schema specific model representation to the transport. +// SetNode injects SBI specific model representation to the transport. // Needed for type assertion. -func (csbi *Csbi) SetNode(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error { - return ytypes.SetNode(schema, root.(*openconfig.Device), path, val, opts...) +func (p *SouthboundPlugin) SetNode(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error { + return p.sbi.SetNode(schema, root, path, val, opts...) +} + +// Schema returns a ygot generated Schema as ytypes.Schema +func (p *SouthboundPlugin) Schema() *ytypes.Schema { + return p.sbi.Schema() +} + +// ID returns the ID of the plugin. +func (p *SouthboundPlugin) ID() uuid.UUID { + return p.sbi.ID() +} + +// SetID sets the ID of the SouthboundPlugin's SBI +func (p *SouthboundPlugin) SetID(id uuid.UUID) { + p.sbi.SetID(id) } -// Unmarshal injects schema specific model representation to the transport. +// Type returns the Southbound's type of the SouthboundPlugin +func (p *SouthboundPlugin) Type() spb.Type { + return p.sbi.Type() +} + +// Unmarshal injects SBI specific model representation to the transport. // Needed for type assertion. -func (csbi *Csbi) Unmarshal(bytes []byte, path *gpb.Path, goStruct ygot.ValidatedGoStruct, opt ...ytypes.UnmarshalOpt) error { - oc := OpenConfig{} - return unmarshal(oc.Schema(), bytes, path, goStruct, opt...) +func (p *SouthboundPlugin) Unmarshal(data []byte, path *gpb.Path, root ygot.ValidatedGoStruct, opts ...ytypes.UnmarshalOpt) error { + return p.sbi.Unmarshal(data, path, root, opts...) } -// Schema is holding the default OpenConfig schema for minimal compatibility -// to gosdn interfaces -func (csbi *Csbi) Schema() *ytypes.Schema { - schema, err := openconfig.Schema() - csbi.schema = schema +// State returns the current state of the plugin. +func (p *SouthboundPlugin) State() plugin.State { + return p.state +} + +// Path returns the path of the plugins binary. +func (p *SouthboundPlugin) Path() string { + return p.BinaryPath +} + +// Manifest returns the Manifest of the plugin. +func (p *SouthboundPlugin) Manifest() *plugin.Manifest { + return p.manifest +} + +// load is a helper function that loads a plugin and casts it to the type of +// southbound.SouthboundInterface. Therefore a SouthboundPlugin has to be provided +// so it can be loaded by using its BinaryPath. The loaded plugin is typecasted to +// southbound.SouthboundInterface and is set as the plugin's southbound interface. +func (p *SouthboundPlugin) load(id uuid.UUID) error { + // load the SouthboundPlugin + symbol, err := LoadPlugin(p.BinaryPath) if err != nil { - log.Fatal(err) + p.state = plugin.FAULTY + return err } - return schema -} + // Typecast the go plugins symbol to southbound.SouthboundInterface + sbi, ok := symbol.(southbound.SouthboundInterface) + if !ok { + p.state = plugin.FAULTY + return &errors.ErrInvalidTypeAssertion{ + Value: reflect.TypeOf(symbol), + Type: reflect.TypeOf((*southbound.SouthboundInterface)(nil)).Elem(), + } + } + // Note(mbauch): We could consider moving this into plugin creation. + // set the ID of the southbound interface to the plugins ID + sbi.SetID(id) + // Update the state of the plugin to LOADED + p.state = plugin.LOADED + // Update the plugins sbi to the loaded go plugin + p.sbi = sbi + + log.WithFields(log.Fields{ + "id": sbi.ID(), + "type": sbi.Type(), + }).Trace("plugin information") -// ID returns the Southbound's UUID -func (csbi *Csbi) ID() uuid.UUID { - return csbi.id + return nil } -// Type returns the Southbound's type -func (csbi *Csbi) Type() spb.Type { - return spb.Type_TYPE_CONTAINERISED +// Update updates the SouthboundPlugin's SBI. +func (p *SouthboundPlugin) Update() error { + updated, err := UpdatePlugin(p) + if err != nil { + return err + } + if updated { + err = p.load(p.ID()) + if err != nil { + return err + } + } + return nil } diff --git a/nucleus/southbound_test.go b/nucleus/southbound_test.go index f483d02ade589fa514ee40587137dfefd712f3e7..344f3e07737c5af04c2b4fc3ed332df8b23c88fa 100644 --- a/nucleus/southbound_test.go +++ b/nucleus/southbound_test.go @@ -167,10 +167,9 @@ func Test_unmarshal(t *testing.T) { bytes := resp.Notification[0].Update[0].Val.GetJsonIetfVal() oc, err := NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { - t.Errorf("unmarshal() error = %v", err) + t.Error(err) return } - if err := unmarshal(oc.Schema(), bytes, resp.Notification[0].Update[0].Path, tt.args.goStruct, tt.args.opt...); err != nil { if !tt.wantErr { t.Errorf("unmarshal() error = %v, wantErr %v", err, tt.wantErr) @@ -187,11 +186,12 @@ func Test_unmarshal(t *testing.T) { func Test_CreateNewUUID(t *testing.T) { sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { - t.Errorf("NewSBI() error = %v", err) + t.Errorf("CreateNewUUID() error = %v", err) + return } if sbi.ID().String() == "" { - t.Errorf("sbi.ID().String() is not set.") + t.Errorf("CreateNewUUID() error = sbi.ID().String() is not set.") } } @@ -199,10 +199,44 @@ func Test_UseProvidedUUID(t *testing.T) { providedSBIId := uuid.New() sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG, providedSBIId) if err != nil { - t.Errorf("NewSBI() error = %v", err) + t.Errorf("UseProvidedUUID() error = %v", err) + return } if sbi.ID() != providedSBIId { - t.Errorf("sbi.ID() is not %s. got=%s", providedSBIId.String(), sbi.ID().String()) + t.Errorf("UseProvidedUUID() error = sbi.ID() is not %s. got=%s", providedSBIId.String(), sbi.ID().String()) + } +} + +func Test_SetID(t *testing.T) { + tests := []struct { + name string + sbiID uuid.UUID + wantErr bool + }{ + { + name: "default", + sbiID: defaultSbiID, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sbi, err := NewSBI(spb.Type_TYPE_OPENCONFIG) + if err != nil { + t.Error(err) + return + } + + if sbi.ID() == defaultSbiID { + t.Errorf("SetID() error = sbi.ID() is already %s", sbi.ID().String()) + } + + sbi.SetID(defaultSbiID) + + if (sbi.ID() != defaultSbiID) != tt.wantErr { + t.Errorf("SetID() error = sbi.ID() is not %s; got=%s", defaultSbiID.String(), sbi.ID().String()) + } + }) } } diff --git a/store/deviceStore.go b/store/deviceStore.go index df0a9ba85b8fdb266e16dc11148ed03eec8d7e75..c76ccc604c0544e3b4177f6310e6cb98821a9033 100644 --- a/store/deviceStore.go +++ b/store/deviceStore.go @@ -143,12 +143,14 @@ func (s *DeviceStore) Delete(id uuid.UUID) error { return nil } +// persist is a helper method that persists the given store.Storable within +// the storage file. func (s *DeviceStore) persist(item store.Storable, name string) error { ensureFilesystemStorePathExists(s.deviceStoreName) _, ok := item.(device.Device) if !ok { - return fmt.Errorf("item is no Device. got=%T", item) + return fmt.Errorf("item is no Device, got=%T", item) } var devicesToPersist []device.Device @@ -156,7 +158,7 @@ func (s *DeviceStore) persist(item store.Storable, name string) error { for _, value := range s.genericStore.Store { dev, ok := value.(device.Device) if !ok { - return fmt.Errorf("item is no Device. got=%T", item) + return fmt.Errorf("item is no Device, got=%T", item) } devicesToPersist = append(devicesToPersist, dev) } diff --git a/store/pndStore.go b/store/pndStore.go index deff904fdf1cea5cba708484164486139fd65977..40f884f3f4f8ef12920768f4924e597279af27d7 100644 --- a/store/pndStore.go +++ b/store/pndStore.go @@ -100,12 +100,14 @@ func (s *PndStore) RemovePendingChannel(id uuid.UUID) { delete(s.pendingChannels, id) } +// persist is a helper method that persists the given store.Storable within +// the storage file. func (s *PndStore) persist(item store.Storable) error { ensureFilesystemStorePathExists(s.pndStoreName) _, ok := item.(networkdomain.NetworkDomain) if !ok { - return fmt.Errorf("item is no NetworkDoman. got=%T", item) + return fmt.Errorf("item is no NetworkDomain, got=%T", item) } var networkDomainsToPersist []LoadedPnd @@ -113,7 +115,7 @@ func (s *PndStore) persist(item store.Storable) error { for _, value := range s.genericStore.Store { networkDomain, ok := value.(networkdomain.NetworkDomain) if !ok { - return fmt.Errorf("item is no Device. got=%T", item) + return fmt.Errorf("item is no NetworkdDomain, got=%T", item) } networkDomainsToPersist = append(networkDomainsToPersist, LoadedPnd{ Name: networkDomain.GetName(), diff --git a/store/sbiStore.go b/store/sbiStore.go index f74d31092cef87ace0682015110a1a0195f8845b..601512d0b00632368fca28e4c9411219714f316c 100644 --- a/store/sbiStore.go +++ b/store/sbiStore.go @@ -1,7 +1,15 @@ package store import ( + "encoding/json" + "fmt" + "io/ioutil" + "reflect" + + spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound" + "code.fbi.h-da.de/danet/gosdn/interfaces/plugin" "code.fbi.h-da.de/danet/gosdn/interfaces/southbound" + "code.fbi.h-da.de/danet/gosdn/interfaces/store" "code.fbi.h-da.de/danet/gosdn/nucleus/errors" "github.com/google/uuid" @@ -10,12 +18,16 @@ import ( // SbiStore is used to store SouthboundInterfaces type SbiStore struct { + sbiStoreName string *genericStore } // NewSbiStore returns a SbiStore -func NewSbiStore() *SbiStore { - return &SbiStore{genericStore: newGenericStore()} +func NewSbiStore(pndUUID uuid.UUID) *SbiStore { + return &SbiStore{ + genericStore: newGenericStore(), + sbiStoreName: fmt.Sprintf("sbi-store-%s.json", pndUUID.String()), + } } // GetSBI takes a SouthboundInterface's UUID and returns the SouthboundInterface. If the requested @@ -37,3 +49,121 @@ func (s *SbiStore) GetSBI(id uuid.UUID) (southbound.SouthboundInterface, error) }).Debug("southbound interface was accessed") return sbi, nil } + +// Add adds a Southbound Interface to the southbound store. +func (s *SbiStore) Add(item store.Storable) error { + if s.Exists(item.ID()) { + return &errors.ErrAlreadyExists{Item: item} + } + + // check if item is a SouthboundInterface + _, ok := item.(southbound.SouthboundInterface) + if !ok { + return &errors.ErrInvalidTypeAssertion{ + Value: reflect.TypeOf(item), + Type: reflect.TypeOf((*southbound.SouthboundInterface)(nil)).Elem(), + } + } + + s.storeLock.Lock() + s.genericStore.Store[item.ID()] = item + s.storeLock.Unlock() + + log.WithFields(log.Fields{ + "type": reflect.TypeOf(item), + "uuid": item.ID(), + }).Debug("storable was added") + + err := s.persist() + if err != nil { + return err + } + + return nil +} + +// persist is a helper method that persists the given store.Storable within +// the storage file. +func (s *SbiStore) persist() error { + err := ensureFilesystemStorePathExists(s.sbiStoreName) + if err != nil { + return err + } + + var southboundInterfacesToPersist []LoadedSbi + + for _, value := range s.genericStore.Store { + southboundInterface, ok := value.(southbound.SouthboundInterface) + if !ok { + return &errors.ErrInvalidTypeAssertion{ + Value: reflect.TypeOf(value), + Type: reflect.TypeOf((*southbound.SouthboundInterface)(nil)).Elem(), + } + } + if southboundInterface.Type() == spb.Type_TYPE_CONTAINERISED || southboundInterface.Type() == spb.Type_TYPE_PLUGIN { + southboundPlugin, ok := southboundInterface.(plugin.Plugin) + if !ok { + return &errors.ErrInvalidTypeAssertion{ + Value: reflect.TypeOf(southboundInterface), + Type: reflect.TypeOf((*plugin.Plugin)(nil)).Elem(), + } + } + southboundInterfacesToPersist = append(southboundInterfacesToPersist, LoadedSbi{ + ID: southboundPlugin.ID(), + Type: southboundInterface.Type().String(), + Path: southboundPlugin.Path(), + }) + } else { + southboundInterfacesToPersist = append(southboundInterfacesToPersist, LoadedSbi{ + ID: southboundInterface.ID(), + Type: southboundInterface.Type().String(), + }) + } + } + + storeDataAsJSON, err := json.MarshalIndent(southboundInterfacesToPersist, "", " ") + if err != nil { + return err + } + + err = ioutil.WriteFile(getCompletePathToFileStore(s.sbiStoreName), storeDataAsJSON, 0644) + if err != nil { + return err + } + + return nil +} + +// LoadedSbi represents a Southbound Interface that was loaeded by using +// the Load() method of the SbiStore. +type LoadedSbi struct { + ID uuid.UUID `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Path string `json:"path,omitempty"` +} + +// Load unmarshals the contents of the storage file associated with a SbiStore +// and returns it as []LoadedSbi. +func (s *SbiStore) Load() ([]LoadedSbi, error) { + var loadedSouthboundInterfaces []LoadedSbi + + err := ensureFilesystemStorePathExists(s.sbiStoreName) + if err != nil { + log.Debug(fmt.Printf("Err: %+v\n", err)) + return loadedSouthboundInterfaces, err + } + + dat, err := ioutil.ReadFile(getCompletePathToFileStore(s.sbiStoreName)) + if err != nil { + log.Debug(fmt.Printf("Err: %+v\n", err)) + return loadedSouthboundInterfaces, err + } + + err = json.Unmarshal(dat, &loadedSouthboundInterfaces) + if err != nil { + log.Debug(fmt.Printf("Err: %+v\n", err)) + return loadedSouthboundInterfaces, err + } + + return loadedSouthboundInterfaces, nil +} diff --git a/store/sbi_store_test.go b/store/sbi_store_test.go index 2dc294639a6947f6c31374a97b75034297647027..282d9ca93da2e1cd5ea23215d1c200587603ef7d 100644 --- a/store/sbi_store_test.go +++ b/store/sbi_store_test.go @@ -1,18 +1,30 @@ package store import ( + "encoding/json" + "io/ioutil" "reflect" "sync" "testing" + spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound" "code.fbi.h-da.de/danet/gosdn/interfaces/southbound" "code.fbi.h-da.de/danet/gosdn/interfaces/store" "code.fbi.h-da.de/danet/gosdn/mocks" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" ) -func Test_sbiStore_get(t *testing.T) { +func newStorableSbi(id uuid.UUID, t spb.Type) *mocks.SouthboundInterface { + s := &mocks.SouthboundInterface{} + s.On("ID").Return(id) + s.On("Type").Return(t) + return s +} + +func Test_sbi_store_GetSBI(t *testing.T) { openConfig := &mocks.SouthboundInterface{} openConfig.On("ID").Return(defaultSbiID) @@ -69,13 +81,229 @@ func Test_sbiStore_get(t *testing.T) { t.Run(tt.name, func(t *testing.T) { s := SbiStore{genericStore: tt.fields.genericStore} - got, err := s.Get(tt.args.id) + got, err := s.GetSBI(tt.args.id) if (err != nil) != tt.wantErr { - t.Errorf("get() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("GetSBI() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("get() got = %v, want %v", got, tt.want) + t.Errorf("GetSBI() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_sbi_store_Add(t *testing.T) { + faultySbi := &mocks.NetworkDomain{} + faultySbi.On("ID").Return(defaultPndID) + + type fields struct { + genericStore *genericStore + } + type args struct { + storable store.Storable + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "default", + fields: fields{ + &genericStore{ + Store: map[uuid.UUID]store.Storable{}, + storeLock: sync.RWMutex{}, + }, + }, + args: args{storable: newStorableSbi(defaultSbiID, spb.Type_TYPE_OPENCONFIG)}, + wantErr: false, + }, + { + name: "does not implement SouthboundInterface", + fields: fields{ + &genericStore{ + Store: map[uuid.UUID]store.Storable{}, + storeLock: sync.RWMutex{}, + }, + }, + args: args{storable: faultySbi}, + wantErr: true, + }, + { + name: "already exists", + fields: fields{ + &genericStore{ + Store: map[uuid.UUID]store.Storable{ + defaultSbiID: newStorableSbi(defaultSbiID, spb.Type_TYPE_OPENCONFIG), + }, + storeLock: sync.RWMutex{}, + }, + }, + args: args{storable: newStorableSbi(defaultSbiID, spb.Type_TYPE_OPENCONFIG)}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := SbiStore{ + sbiStoreName: "sbi-store_test.json", + genericStore: tt.fields.genericStore, + } + + err := s.Add(tt.args.storable) + if (err != nil) != tt.wantErr { + t.Errorf("Add() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func Test_sbi_store_persist(t *testing.T) { + faultySbi := &mocks.NetworkDomain{} + faultySbi.On("ID").Return(defaultPndID) + + csbi := &mocks.Csbi{} + csbi.On("ID").Return(defaultSbiID) + csbi.On("Type").Return(spb.Type_TYPE_CONTAINERISED) + csbi.On("Path").Return("a/test/path/") + + plugin := &mocks.Csbi{} + plugin.On("ID").Return(defaultSbiID) + plugin.On("Type").Return(spb.Type_TYPE_PLUGIN) + plugin.On("Path").Return("a/test/path/") + + // options to apply to cmp.Equal + opts := []cmp.Option{ + // compare option to treat slices of length zero as equal + cmpopts.EquateEmpty(), + // sort the slices based on the ID + cmpopts.SortSlices(func(x, y LoadedSbi) bool { + return x.ID.ID() < y.ID.ID() + }), + } + + type fields struct { + genericStore *genericStore + } + tests := []struct { + name string + fields fields + want []LoadedSbi + wantErr bool + }{ + { + name: "default", + fields: fields{ + &genericStore{ + Store: map[uuid.UUID]store.Storable{ + defaultSbiID: newStorableSbi(defaultSbiID, spb.Type_TYPE_OPENCONFIG), + }, + storeLock: sync.RWMutex{}, + }, + }, + want: []LoadedSbi{{ID: defaultSbiID, Type: spb.Type_TYPE_OPENCONFIG.String()}}, + wantErr: false, + }, + { + name: "multiple items in store", + fields: fields{ + &genericStore{ + Store: map[uuid.UUID]store.Storable{ + defaultPndID: newStorableSbi(defaultPndID, spb.Type_TYPE_OPENCONFIG), + defaultSbiID: newStorableSbi(defaultSbiID, spb.Type_TYPE_OPENCONFIG), + }, + storeLock: sync.RWMutex{}, + }, + }, + want: []LoadedSbi{ + {ID: defaultSbiID, Type: spb.Type_TYPE_OPENCONFIG.String()}, + {ID: defaultPndID, Type: spb.Type_TYPE_OPENCONFIG.String()}, + }, + wantErr: false, + }, + { + name: "storable is not of type SouthboundInterface", + fields: fields{ + &genericStore{ + Store: map[uuid.UUID]store.Storable{ + defaultPndID: faultySbi, + }, + storeLock: sync.RWMutex{}, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "storable is of type csbi", + fields: fields{ + &genericStore{ + Store: map[uuid.UUID]store.Storable{ + defaultSbiID: csbi, + }, + storeLock: sync.RWMutex{}, + }, + }, + want: []LoadedSbi{{ID: defaultSbiID, Type: spb.Type_TYPE_CONTAINERISED.String(), Path: csbi.Path()}}, + wantErr: false, + }, + { + name: "storable is of type plugin", + fields: fields{ + &genericStore{ + Store: map[uuid.UUID]store.Storable{ + defaultSbiID: plugin, + }, + storeLock: sync.RWMutex{}, + }, + }, + want: []LoadedSbi{{ID: defaultSbiID, Type: spb.Type_TYPE_PLUGIN.String(), Path: csbi.Path()}}, + wantErr: false, + }, + { + name: "type of spb.CONTAINERISED but does not implement Csbi", + fields: fields{ + &genericStore{ + Store: map[uuid.UUID]store.Storable{ + defaultSbiID: newStorableSbi(defaultSbiID, spb.Type_TYPE_CONTAINERISED), + }, + storeLock: sync.RWMutex{}, + }, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := SbiStore{ + sbiStoreName: "sbi-store_test.json", + genericStore: tt.fields.genericStore} + + err := s.persist() + if (err != nil) != tt.wantErr { + t.Errorf("persist() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err == nil { + var got []LoadedSbi + dat, err := ioutil.ReadFile(getCompletePathToFileStore(s.sbiStoreName)) + if err != nil { + t.Errorf("persist() error = %v", err) + } + + err = json.Unmarshal(dat, &got) + if err != nil { + t.Errorf("persist() error = %v", err) + } + + if !cmp.Equal(got, tt.want, opts...) { + t.Errorf("persist() got = %v, want %v", got, tt.want) + } } }) } diff --git a/store/utils.go b/store/utils.go index 074edab53ca811f440092b1c05530f4b3c43111a..3d96d81b323a71020ef6435840ef923e1d490daa 100644 --- a/store/utils.go +++ b/store/utils.go @@ -1,7 +1,6 @@ package store import ( - "fmt" "os" "path/filepath" @@ -70,5 +69,5 @@ func ensureDirExists(fileName string) error { } func getCompletePathToFileStore(storeName string) string { - return fmt.Sprintf("%s/%s", pathToStores, storeName) + return filepath.Join(pathToStores, storeName) } diff --git a/test/integration/nucleusIntegration_test.go b/test/integration/nucleusIntegration_test.go index 026476882154ff7fd82378998b94a8deda2681ea..cc59d807c569212b113729d80ffd1cebf670d260 100644 --- a/test/integration/nucleusIntegration_test.go +++ b/test/integration/nucleusIntegration_test.go @@ -135,19 +135,19 @@ func TestGnmi_SetInvalidIntegration(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - oc, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) + sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { - t.Errorf("NewSBI() error = %v", err) + t.Errorf("SetInvalidIntegration() error = %v", err) + return } - g, err := nucleus.NewTransport(tt.fields.opt, oc) - - if err != nil { - t.Errorf("NewGnmiTransport() error = %v, wantErr %v", err, tt.wantErr) + g, err := nucleus.NewTransport(tt.fields.opt, sbi) + if (err != nil) != tt.wantErr { + t.Errorf("SetInvalidIntegration() error = %v, wantErr %v", err, tt.wantErr) return } err = g.Set(tt.args.ctx, tt.args.payload) if (err != nil) != tt.wantErr { - t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("SetInvalidIntegration() error = %v, wantErr %v", err, tt.wantErr) return } }) @@ -162,8 +162,8 @@ func TestGnmi_SetValidIntegration(t *testing.T) { sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("SetValidIntegration() err = %v", err) + return } - opt := &tpb.TransportOption{ Address: testAddress, Username: testUsername, @@ -304,13 +304,12 @@ func TestGnmi_GetIntegration(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - oc, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) + sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("Get() error = %v", err) return } - g, err := nucleus.NewTransport(tt.fields.opt, oc) - + g, err := nucleus.NewTransport(tt.fields.opt, sbi) if err != nil { t.Error(err) return @@ -419,13 +418,12 @@ func TestGnmi_SubscribeIntegration(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var wantErr = tt.wantErr - oc, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) + sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("Subscribe() error = %v", err) return } - g, err := nucleus.NewTransport(tt.fields.opt, oc) - + g, err := nucleus.NewTransport(tt.fields.opt, sbi) if err != nil { t.Error(err) return @@ -502,12 +500,12 @@ func TestGnmi_CapabilitiesIntegration(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - oc, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) + sbi, err := nucleus.NewSBI(spb.Type_TYPE_OPENCONFIG) if err != nil { t.Errorf("Capabilities() error = %v", err) return } - tr, err := nucleus.NewTransport(tt.fields.opt, oc) + tr, err := nucleus.NewTransport(tt.fields.opt, sbi) if err != nil { t.Error(err) return diff --git a/test/plugin/faulty/gostructs.go b/test/plugin/faulty/gostructs.go new file mode 100644 index 0000000000000000000000000000000000000000..e0c548a5369a74adf57d8f652569bafe53ea03a7 --- /dev/null +++ b/test/plugin/faulty/gostructs.go @@ -0,0 +1,47 @@ +package main + +import ( + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/goyang/pkg/yang" + "github.com/openconfig/ygot/ygot" + "github.com/openconfig/ygot/ytypes" + + spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound" + "github.com/google/uuid" +) + +type FaultyPlugin struct { + schema *ytypes.Schema + id uuid.UUID +} + +// SetNode injects schema specific model representation to the transport. +// Needed for type assertion. +func (fp *FaultyPlugin) SetNode(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error { + return nil +} + +// Unmarshal injects schema specific model representation to the transport. +// Needed for type assertion. +func (fp *FaultyPlugin) Unmarshal(bytes []byte, path *gpb.Path, goStruct ygot.ValidatedGoStruct, opt ...ytypes.UnmarshalOpt) error { + return nil +} + +func (fp *FaultyPlugin) Schema() *ytypes.Schema { + return nil +} + +// SetID sets the ID of the cSBI to the provided UUID +func (fp *FaultyPlugin) SetID(id uuid.UUID) { + fp.id = id +} + +// ID returns the Southbound's UUID +func (fp *FaultyPlugin) ID() uuid.UUID { + return fp.id +} + +// Type returns the Southbound's type +func (fp *FaultyPlugin) Type() spb.Type { + return spb.Type_TYPE_PLUGIN +} diff --git a/test/plugin/faulty/plugin.yml b/test/plugin/faulty/plugin.yml new file mode 100644 index 0000000000000000000000000000000000000000..e20c02acb00783cf1f827edbab5564e5610725fd --- /dev/null +++ b/test/plugin/faulty/plugin.yml @@ -0,0 +1 @@ +version: "1.0.1" diff --git a/test/plugin/gostructs.go b/test/plugin/gostructs.go new file mode 100644 index 0000000000000000000000000000000000000000..fbd757b210b0fdcd47b0c1164dd9b41d3284228c --- /dev/null +++ b/test/plugin/gostructs.go @@ -0,0 +1,49 @@ +package main + +import ( + gpb "github.com/openconfig/gnmi/proto/gnmi" + "github.com/openconfig/goyang/pkg/yang" + "github.com/openconfig/ygot/ygot" + "github.com/openconfig/ygot/ytypes" + + spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound" + "github.com/google/uuid" +) + +var PluginSymbol Csbi + +type Csbi struct { + schema *ytypes.Schema + id uuid.UUID +} + +// SetNode injects schema specific model representation to the transport. +// Needed for type assertion. +func (csbi *Csbi) SetNode(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error { + return nil +} + +// Unmarshal injects schema specific model representation to the transport. +// Needed for type assertion. +func (csbi *Csbi) Unmarshal(bytes []byte, path *gpb.Path, goStruct ygot.ValidatedGoStruct, opt ...ytypes.UnmarshalOpt) error { + return nil +} + +func (csbi *Csbi) Schema() *ytypes.Schema { + return nil +} + +// SetID sets the ID of the cSBI to the provided UUID +func (csbi *Csbi) SetID(id uuid.UUID) { + csbi.id = id +} + +// ID returns the Southbound's UUID +func (csbi *Csbi) ID() uuid.UUID { + return csbi.id +} + +// Type returns the Southbound's type +func (csbi *Csbi) Type() spb.Type { + return spb.Type_TYPE_PLUGIN +} diff --git a/test/plugin/plugin.yml b/test/plugin/plugin.yml new file mode 100644 index 0000000000000000000000000000000000000000..bef9de49acb8ef101e286146553f5847f0308392 --- /dev/null +++ b/test/plugin/plugin.yml @@ -0,0 +1,3 @@ +name: test +author: goSDN-Team +version: "v1.0.5"