Skip to content
Snippets Groups Projects
Commit 8e2fe42b authored by Andre Sterba's avatar Andre Sterba Committed by Manuel Kieweg
Browse files

Let user set a name for a device or autogenerate it

parent a94be2bb
No related branches found
No related tags found
9 merge requests!246Develop,!245Develop into Master,!244Master into develop2 into master,!219Draft: Testing,!214Test pipelines,!195DO NOT MERGE 2,!194DO NOT MERGE! just for testing,!150Let user set a name for a device or autogenerate it,!138Develop
......@@ -18,20 +18,20 @@ import (
// Subscribe starts a gNMI subscriber requersting the specified paths on the target and
// logs the response to stdout. Only 'stream' mode with 'sample' operation supported.
func Subscribe(a, u, p string, sample, heartbeat int64, args ...string) error {
func Subscribe(address, username, password, deviceName string, sample, heartbeat int64, args ...string) error {
sbi := &nucleus.OpenConfig{}
tOpts := &nucleus.GnmiTransportOptions{
Config: gnmi.Config{
Addr: a,
Username: u,
Password: p,
Addr: address,
Username: username,
Password: password,
Encoding: gpb.Encoding_JSON_IETF,
},
SetNode: sbi.SetNode(),
RespChan: make(chan *gpb.SubscribeResponse),
}
device, err := nucleus.NewDevice(sbi, tOpts)
device, err := nucleus.NewDevice(sbi, tOpts, deviceName)
if err != nil {
return err
}
......@@ -46,7 +46,7 @@ func Subscribe(a, u, p string, sample, heartbeat int64, args ...string) error {
HeartbeatInterval: uint64(heartbeat * time.Second.Nanoseconds()),
Paths: gnmi.SplitPaths(args),
Origin: "",
Target: a,
Target: address,
}
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGILL, syscall.SIGTERM)
......@@ -59,5 +59,6 @@ func Subscribe(a, u, p string, sample, heartbeat int64, args ...string) error {
fmt.Println("awaiting signal")
<-done
fmt.Println("exiting")
return nil
}
......@@ -36,6 +36,8 @@ import (
"github.com/spf13/cobra"
)
var deviceName string
// addDeviceCmd represents the addDevice command
var addDeviceCmd = &cobra.Command{
Use: "add-device",
......@@ -53,10 +55,13 @@ if they diverge from the default credentials.`,
"username="+username,
"sbi="+cliSbi,
"pnd="+cliPnd,
"name="+deviceName,
)
},
}
func init() {
cliCmd.AddCommand(addDeviceCmd)
addDeviceCmd.PersistentFlags().StringVarP(&deviceName, "device-name", "n", "", "Human readable device name.")
}
......@@ -43,12 +43,12 @@ var getDeviceCmd = &cobra.Command{
Short: "gets device information from the controller",
Long: `Gets device information from the controller.
Device UUID needs to be specified as positional argument.`,
Device UUID or name needs to be specified as positional argument.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cli.HTTPGet(
apiEndpoint,
"getDevice",
"uuid="+args[0],
"identifier="+args[0],
"sbi="+cliSbi,
"pnd="+cliPnd,
)
......
......
File changed. Contains only whitespace changes. Show whitespace changes.
File changed. Contains only whitespace changes. Show whitespace changes.
File changed. Contains only whitespace changes. Show whitespace changes.
......@@ -47,7 +47,7 @@ var subscribeCmd = &cobra.Command{
Only 'stream' mode with 'sample' operation supported.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cli.Subscribe(address, username, password, sampleInterval, heartbeatInterval, args...)
return cli.Subscribe(address, username, password, "", sampleInterval, heartbeatInterval, args...)
},
}
......
......
......@@ -5,6 +5,7 @@ go 1.14
require (
code.fbi.h-da.de/cocsn/yang-models v0.0.7
github.com/aristanetworks/goarista v0.0.0-20201120222254-94a892eb0c6a
github.com/docker/docker v1.13.1
github.com/golang/protobuf v1.5.0
github.com/google/gnxi v0.0.0-20201221102247-c26672548161
github.com/google/uuid v1.1.2
......
......
package gosdn
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"time"
"code.fbi.h-da.de/cocsn/gosdn/forks/goarista/gnmi"
"code.fbi.h-da.de/cocsn/gosdn/nucleus"
"code.fbi.h-da.de/cocsn/gosdn/nucleus/errors"
"code.fbi.h-da.de/cocsn/gosdn/nucleus/types"
"context"
"fmt"
"github.com/google/uuid"
gpb "github.com/openconfig/gnmi/proto/gnmi"
log "github.com/sirupsen/logrus"
"io"
"net/http"
"net/url"
"time"
)
var apiOpmap = map[string]types.Operation{
......@@ -119,15 +122,26 @@ func httpApi(writer http.ResponseWriter, request *http.Request) {
SetNode: httpSbi.SetNode(),
Unmarshal: httpSbi.(*nucleus.OpenConfig).Unmarshal(),
RespChan: make(chan *gpb.SubscribeResponse),
})
},
query.Get("name"),
)
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
log.Error(err)
return
}
err = httpPnd.AddDevice(d)
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
log.Error(err)
return
}
writer.WriteHeader(http.StatusCreated)
fmt.Fprintf(writer, "device added\n")
fmt.Fprintf(writer, "Name: %s\n", d.Name)
fmt.Fprintf(writer, "UUID: %v\n", d.UUID)
case "request":
err = httpPnd.Request(id, query.Get("path"))
......@@ -156,7 +170,9 @@ func httpApi(writer http.ResponseWriter, request *http.Request) {
}
writer.WriteHeader(http.StatusOK)
case "getDevice":
device, err := httpPnd.MarshalDevice(id)
deviceIdentifier := query.Get("identifier")
device, err := httpPnd.MarshalDevice(deviceIdentifier)
if err != nil {
switch err.(type) {
case *errors.ErrNotFound:
......@@ -165,10 +181,13 @@ func httpApi(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusInternalServerError)
}
log.Error(err)
return
}
writer.Header().Set("Content-Type", "application/json")
fmt.Fprintf(writer, "%v", device)
case "getIDs":
pnds := c.pndc.UUIDs()
......
......
......@@ -35,6 +35,7 @@ func testSetupHTTP() {
}
args = "&uuid=" + mdid.String() + "&pnd=" + defaultPndID.String() + "&sbi=" + defaultSbiID.String()
argsNotFound = "&uuid=" + uuid.New().String() + "&pnd=" + defaultPndID.String() + "&sbi=" + defaultSbiID.String()
argsNotFoundGetDevice = "&identifier=" + uuid.New().String() + "&pnd=" + defaultPndID.String() + "&sbi=" + defaultSbiID.String()
if err := c.sbic.Add(sbi); err != nil {
log.Fatal(err)
}
......@@ -119,7 +120,7 @@ func Test_httpApi(t *testing.T) {
},
{
name: "get-device not found",
request: apiEndpoint + "/api?q=getDevice" + argsNotFound,
request: apiEndpoint + "/api?q=getDevice" + argsNotFoundGetDevice,
want: &http.Response{StatusCode: http.StatusNotFound},
wantErr: false,
},
......
......
......@@ -30,6 +30,7 @@ var httpTestDevice nucleus.Device
var args string
var argsNotFound string
var argsNotFoundGetDevice string
var mockContext = mock.MatchedBy(func(ctx context.Context) bool { return true })
......
......
......@@ -2,6 +2,7 @@ package nucleus
import (
"code.fbi.h-da.de/cocsn/gosdn/nucleus/errors"
"github.com/docker/docker/pkg/namesgenerator"
"github.com/google/uuid"
"github.com/openconfig/ygot/ygot"
)
......@@ -20,20 +21,28 @@ type Device struct {
// Transport is the device's Transport implementation
Transport Transport
// Name is the device's human readable name
Name string
}
// NewDevice creates a Device
func NewDevice(sbi SouthboundInterface, opts TransportOptions) (*Device, error) {
func NewDevice(sbi SouthboundInterface, opts TransportOptions, name string) (*Device, error) {
var transport Transport
var err error
switch o := opts.(type) {
if name == "" {
name = namesgenerator.GetRandomName(0)
}
switch opts := opts.(type) {
case *GnmiTransportOptions:
transport, err = NewGnmiTransport(o)
transport, err = NewGnmiTransport(opts)
if err != nil {
return nil, err
}
default:
return nil, &errors.ErrInvalidTransportOptions{Opt: o}
return nil, &errors.ErrInvalidTransportOptions{Opt: opts}
}
return &Device{
......@@ -41,6 +50,7 @@ func NewDevice(sbi SouthboundInterface, opts TransportOptions) (*Device, error)
GoStruct: sbi.Schema().Root,
SBI: sbi,
Transport: transport,
Name: name,
}, nil
}
......
......
......@@ -16,6 +16,7 @@ func TestDevice_Id(t *testing.T) {
SBI SouthboundInterface
Transport Transport
UUID uuid.UUID
Name string
}
tests := []struct {
name string
......@@ -37,6 +38,7 @@ func TestDevice_Id(t *testing.T) {
SBI: tt.fields.SBI,
Transport: tt.fields.Transport,
UUID: tt.fields.UUID,
Name: tt.fields.Name,
}
if got := d.ID(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ID() = %v, want %v", got, tt.want)
......@@ -50,6 +52,7 @@ func TestNewDevice(t *testing.T) {
type args struct {
sbi SouthboundInterface
opts TransportOptions
name string
}
tests := []struct {
name string
......@@ -68,6 +71,7 @@ func TestNewDevice(t *testing.T) {
Password: "test",
},
},
name: "MyDevice",
},
want: &Device{
GoStruct: &openconfig.Device{},
......@@ -82,12 +86,13 @@ func TestNewDevice(t *testing.T) {
},
},
},
Name: "MyDevice",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewDevice(tt.args.sbi, tt.args.opts)
got, err := NewDevice(tt.args.sbi, tt.args.opts, tt.args.name)
if err != nil {
t.Error(err)
}
......
......
......@@ -41,6 +41,15 @@ func (e *ErrAlreadyExists) Error() string {
return fmt.Sprintf("%v already exists", e.Item)
}
// ErrInvalidUUID implements the Error interface and is called if a UUID is not valid.
type ErrInvalidUUID struct {
DeviceName string
}
func (e *ErrInvalidUUID) Error() string {
return fmt.Sprintf("UUID not valid")
}
// ErrInvalidTypeAssertion implements the Error interface and is called if the
// type of a storable item does not correspond to the expected type.
type ErrInvalidTypeAssertion struct {
......
......
......@@ -129,7 +129,7 @@ func newPnd() pndImplementation {
name: "default",
description: "default test pnd",
sbic: SbiStore{store{}},
devices: DeviceStore{store{}},
devices: NewDeviceStore(),
pendingChanges: ChangeStore{store{}},
committedChanges: ChangeStore{store{}},
confirmedChanges: ChangeStore{store{}},
......
......
......@@ -23,7 +23,7 @@ type PrincipalNetworkDomain interface {
AddSbi(interface{}) error
RemoveSbi(uuid.UUID) error
AddDevice(interface{}) error
GetDevice(uuid uuid.UUID) (ygot.GoStruct, error)
GetDevice(identifier string) (*Device, error)
RemoveDevice(uuid.UUID) error
Devices() []uuid.UUID
ChangeOND(uuid uuid.UUID, operation interface{}, path string, value ...string) error
......@@ -31,7 +31,7 @@ type PrincipalNetworkDomain interface {
RequestAll(string) error
GetName() string
GetDescription() string
MarshalDevice(uuid.UUID) (string, error)
MarshalDevice(string) (string, error)
ContainsDevice(uuid.UUID) bool
GetSBIs() interface{}
ID() uuid.UUID
......@@ -47,7 +47,7 @@ func NewPND(name, description string, id uuid.UUID, sbi SouthboundInterface) (Pr
name: name,
description: description,
sbic: SbiStore{store{}},
devices: DeviceStore{store{}},
devices: NewDeviceStore(),
pendingChanges: ChangeStore{store{}},
committedChanges: ChangeStore{store{}},
confirmedChanges: ChangeStore{store{}},
......@@ -64,7 +64,7 @@ type pndImplementation struct {
name string
description string
sbic SbiStore
devices DeviceStore
devices *DeviceStore
pendingChanges ChangeStore
committedChanges ChangeStore
confirmedChanges ChangeStore
......@@ -184,12 +184,20 @@ func (pnd *pndImplementation) AddDevice(device interface{}) error {
return pnd.addDevice(d)
}
func (pnd *pndImplementation) GetDevice(uuid uuid.UUID) (ygot.GoStruct, error) {
d, err := pnd.devices.Get(uuid)
func (pnd *pndImplementation) GetDevice(identifier string) (*Device, error) {
d, err := pnd.devices.Get(FromString(identifier))
if err != nil {
return nil, err
}
return ygot.DeepCopy(d.GoStruct)
copiedGoStruct, err := ygot.DeepCopy(d.GoStruct)
if err != nil {
return nil, err
}
copiedDevice := &Device{Name: d.Name, UUID: d.UUID, GoStruct: copiedGoStruct}
return copiedDevice, nil
}
// RemoveDevice removes a device from the PND
......@@ -212,36 +220,40 @@ func (pnd *pndImplementation) removeSbi(id uuid.UUID) error {
}
func (pnd *pndImplementation) addDevice(device *Device) error {
return pnd.devices.Add(device)
err := pnd.devices.Add(device, device.Name)
if err != nil {
return err
}
func (pnd *pndImplementation) getDevice(id uuid.UUID) (*Device, error) {
return pnd.devices.Get(id)
return nil
}
func (pnd *pndImplementation) removeDevice(id uuid.UUID) error {
return pnd.devices.Delete(id)
}
func (pnd *pndImplementation) MarshalDevice(uuid uuid.UUID) (string, error) {
d, err := pnd.getDevice(uuid)
func (pnd *pndImplementation) MarshalDevice(identifier string) (string, error) {
foundDevice, err := pnd.devices.Get(FromString(identifier))
if err != nil {
return "", err
}
jsonTree, err := json.MarshalIndent(d.GoStruct, "", "\t")
jsonTree, err := json.MarshalIndent(foundDevice.GoStruct, "", "\t")
if err != nil {
return "", err
}
log.WithFields(log.Fields{
"pnd": pnd.id,
"device": uuid,
"Identifier": identifier,
"Name": foundDevice.Name,
}).Info("marshalled device")
return string(jsonTree), nil
}
// Request sends a get request to a specific device
func (pnd *pndImplementation) Request(uuid uuid.UUID, path string) error {
d, err := pnd.getDevice(uuid)
d, err := pnd.devices.Get(FromString(uuid.String()))
if err != nil {
return err
}
......@@ -273,12 +285,15 @@ func (pnd *pndImplementation) RequestAll(path string) error {
// ChangeOND creates a change from the provided Operation, path and value. The Change is pending and
func (pnd *pndImplementation) ChangeOND(uuid uuid.UUID, operation interface{}, path string, value ...string) error {
d, err := pnd.getDevice(uuid)
d, err := pnd.devices.Get(FromString(uuid.String()))
if err != nil {
return err
}
cpy, err := ygot.DeepCopy(d.GoStruct)
ygot.BuildEmptyTree(cpy)
if err != nil {
return err
}
p, err := ygot.StringToStructuredPath(path)
if err != nil {
......
......
......@@ -224,7 +224,7 @@ func Test_pndImplementation_ContainsDevice(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
pnd := newPnd()
if tt.name != "fails empty" {
if err := pnd.devices.Add(tt.args.device); err != nil {
if err := pnd.devices.Add(tt.args.device, "test"); err != nil {
t.Error(err)
}
}
......@@ -243,7 +243,7 @@ func Test_pndImplementation_Destroy(t *testing.T) {
name string
description string
sbi SbiStore
devices DeviceStore
devices *DeviceStore
}
tests := []struct {
name string
......@@ -347,7 +347,7 @@ func Test_pndImplementation_MarshalDevice(t *testing.T) {
if err := pnd.addDevice(d); err != nil {
t.Error(err)
}
got, err := pnd.MarshalDevice(tt.args.uuid)
got, err := pnd.MarshalDevice(tt.args.uuid.String())
if (err != nil) != tt.wantErr {
t.Errorf("MarshalDevice() error = %v, wantErr %v", err, tt.wantErr)
return
......@@ -413,7 +413,7 @@ func Test_pndImplementation_RemoveSbi(t *testing.T) {
name: "test-remove-sbi",
description: "test-remove-sbi",
sbic: SbiStore{store{}},
devices: DeviceStore{store{}},
devices: NewDeviceStore(),
id: defaultPndID,
}
if tt.name != "fails empty" {
......@@ -661,7 +661,7 @@ func Test_pndImplementation_ChangeOND(t *testing.T) {
func Test_pndImplementation_GetDevice(t *testing.T) {
pnd := newPnd()
sbi := NewSBI(types.Openconfig)
d, err := NewDevice(sbi, &GnmiTransportOptions{})
d, err := NewDevice(sbi, &GnmiTransportOptions{}, "")
if err != nil {
t.Error(err)
return
......@@ -694,14 +694,68 @@ func Test_pndImplementation_GetDevice(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := pnd.GetDevice(tt.args.uuid)
foundDevice, err := pnd.GetDevice(tt.args.uuid.String())
if (err != nil) != tt.wantErr {
t.Errorf("GetDevice() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetDevice() got = %v, want %v", got, tt.want)
if foundDevice != nil {
if !reflect.DeepEqual(foundDevice.GoStruct, tt.want) {
t.Errorf("GetDevice() got = %v, want %v", foundDevice.GoStruct, tt.want)
}
}
})
}
}
func Test_pndImplementation_GetDeviceByName(t *testing.T) {
p := newPnd()
sbi := NewSBI(types.Openconfig)
d, err := NewDevice(sbi, &GnmiTransportOptions{}, "my-device")
if err != nil {
t.Error(err)
return
}
if err = p.addDevice(d); err != nil {
t.Error(err)
return
}
type args struct {
name string
}
tests := []struct {
name string
args args
want ygot.GoStruct
wantErr bool
}{
{
name: "default",
args: args{name: d.Name},
want: sbi.Schema().Root,
wantErr: false,
},
{
name: "device not found",
args: args{name: "test-device"},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
foundDevice, err := p.GetDevice(tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("GetDeviceByName() error = %v, wantErr %v", err, tt.wantErr)
return
}
if foundDevice != nil {
if !reflect.DeepEqual(foundDevice.GoStruct, tt.want) {
t.Errorf("GetDeviceByName() got = %v, want %v", foundDevice.GoStruct, tt.want)
}
}
})
}
}
......
......
package nucleus
import (
"fmt"
"reflect"
"sync"
......@@ -43,7 +44,7 @@ func NewSbiStore() *SbiStore {
// NewDeviceStore returns a DeviceStore
func NewDeviceStore() *DeviceStore {
return &DeviceStore{store{}}
return &DeviceStore{store: store{}, deviceNameToUUIDLookup: make(map[string]uuid.UUID)}
}
// NewChangeStore returns a ChangeStore
......@@ -170,13 +171,33 @@ func (s PndStore) Get(id uuid.UUID) (PrincipalNetworkDomain, error) {
// DeviceStore is used to store Devices
type DeviceStore struct {
deviceNameToUUIDLookup map[string]uuid.UUID
store
}
// Get takes a Device's UUID and returns the Device. If the requested
// Device does not exist an error is returned.
func (s DeviceStore) Get(id uuid.UUID) (*Device, error) {
item, err := s.store.Get(id)
func (s DeviceStore) Get(id uuid.UUID, parseErrors ...error) (*Device, error) {
var foundID uuid.UUID
foundID = id
for _, parseErrs := range parseErrors {
if parseErrs != nil {
switch e := parseErrs.(type) {
case *errors.ErrInvalidUUID:
myID, ok := s.deviceNameToUUIDLookup[e.DeviceName]
if !ok {
log.Debug(fmt.Sprintf("no device named %s found", foundID))
return nil, &errors.ErrNotFound{}
}
foundID = myID
}
}
}
item, err := s.store.Get(foundID)
if err != nil {
return nil, err
}
......@@ -189,10 +210,72 @@ func (s DeviceStore) Get(id uuid.UUID) (*Device, error) {
}
log.WithFields(log.Fields{
"uuid": id,
"name": device.Name,
}).Debug("device was accessed")
return device, nil
}
// FromString is a helper to check if a provided string as a valid UUID or a name.
func FromString(id string) (uuid.UUID, error) {
idAsUUID, err := uuid.Parse(id)
// id is no UUID therefore it could be a device name.
// The name will be returned within the error.
if err != nil {
log.WithFields(log.Fields{
"identifier": id,
}).Debug(err)
return uuid.Nil, &errors.ErrInvalidUUID{DeviceName: id}
}
return idAsUUID, nil
}
// Add adds a device to the device store.
// It also adds the name of the device to the lookup table.
func (s DeviceStore) Add(item Storable, name string) error {
if s.Exists(item.ID()) {
return &errors.ErrAlreadyExists{Item: item}
}
s.deviceNameToUUIDLookup[name] = item.ID()
storeLock.Lock()
s.store[item.ID()] = item
storeLock.Unlock()
log.WithFields(log.Fields{
"type": reflect.TypeOf(item),
"uuid": item.ID(),
}).Debug("storable was added")
return nil
}
// Delete deletes a device from the device store.
func (s DeviceStore) Delete(id uuid.UUID) error {
if !s.Exists(id) {
return &errors.ErrNotFound{ID: id}
}
storeLock.Lock()
delete(s.store, id)
storeLock.Unlock()
for key, value := range s.deviceNameToUUIDLookup {
if value == id {
delete(s.deviceNameToUUIDLookup, key)
}
}
log.WithFields(log.Fields{
"uuid": id,
}).Debug("storable was deleted")
return nil
}
// ChangeStore is used to store Changes
type ChangeStore struct {
store
......
......
......@@ -424,8 +424,10 @@ func Test_deviceStore_get(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
s := DeviceStore{
store: tt.fields.store,
deviceNameToUUIDLookup: make(map[string]uuid.UUID),
}
got, err := s.Get(tt.args.id)
got, err := s.Get(FromString(tt.args.id.String()))
if (err != nil) != tt.wantErr {
t.Errorf("get() error = %v, wantErr %v", err, tt.wantErr)
return
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment