diff --git a/gnmi/server.go b/gnmi/server.go index 243573ae..3ee15ce8 100644 --- a/gnmi/server.go +++ b/gnmi/server.go @@ -296,6 +296,17 @@ func (s *Server) toGoStruct(jsonTree map[string]interface{}) (ygot.ValidatedGoSt return goStruct, nil } +// ConfigAsJSON takes the current configuration of the running server and +// returns it in a RFC7951 compliant json string. +func (s *Server) ConfigAsJSON() (string, error) { + return ygot.EmitJSON(s.config, &ygot.EmitJSONConfig{ + Format: ygot.RFC7951, + RFC7951Config: &ygot.RFC7951JSONConfig{ + AppendModuleName: true, + }, + }) +} + // getGNMIServiceVersion returns a pointer to the gNMI service version string. // The method is non-trivial because of the way it is defined in the proto file. func getGNMIServiceVersion() (*string, error) { diff --git a/gnmi/server_test.go b/gnmi/server_test.go index 7e98c4e7..f53c61c7 100644 --- a/gnmi/server_test.go +++ b/gnmi/server_test.go @@ -1696,3 +1696,64 @@ func updateLess(a, b *pb.Update) bool { } return pathA < pathB } + +// jsonBytesEqual is a helper function to compare two json strings for +// equality. +func jsonBytesEqual(a, b []byte) (bool, error) { + var j, j2 interface{} + if err := json.Unmarshal(a, &j); err != nil { + return false, err + } + if err := json.Unmarshal(b, &j2); err != nil { + return false, err + } + return reflect.DeepEqual(j2, j), nil +} + +// TestConfigAsJSON validates that the method `ConfigAsJSON` returns a json +// string that meets the running configuration of the server in a fashion that +// can be loaded back into the server. +func TestConfigToJSON(t *testing.T) { + jsonConfigRoot := `{ + "openconfig-system:system": { + "openconfig-openflow:openflow": { + "agent": { + "config": { + "failure-mode": "SECURE", + "max-backoff": 10 + } + } + } + }, + "openconfig-platform:components": { + "component": [ + { + "config": { + "name": "swpri1-1-1" + }, + "name": "swpri1-1-1" + } + ] + } + }` + + s, err := NewServer(model, []byte(jsonConfigRoot), nil) + if err != nil { + t.Fatalf("error in creating server: %v", err) + } + + res, err := s.ConfigAsJSON() + if err != nil { + t.Fatalf("error in creating json from model: %v", err) + } + + areEqual, err := jsonBytesEqual([]byte(jsonConfigRoot), []byte(res)) + if err != nil { + t.Fatalf("error in comparing json bytes: %v", err) + } + + if (!areEqual) { + t.Errorf("config mismatch!\n Got: %s\n Wanted: %s", res, jsonConfigRoot) + } + +} diff --git a/gnmi_target/gnmi_target.go b/gnmi_target/gnmi_target.go index c48432f4..2ada2840 100644 --- a/gnmi_target/gnmi_target.go +++ b/gnmi_target/gnmi_target.go @@ -22,7 +22,9 @@ import ( "io/ioutil" "net" "os" + "os/signal" "reflect" + "syscall" log "github.com/golang/glog" "golang.org/x/net/context" @@ -43,6 +45,7 @@ import ( var ( bindAddr = flag.String("bind_address", ":9339", "Bind to address:port or just :port") configFile = flag.String("config", "", "IETF JSON file for target startup config") + saveOnExit = flag.Bool("save_on_exit", false, "Save the config before exiting the server.") ) type server struct { @@ -79,7 +82,7 @@ func (s *server) Set(ctx context.Context, req *pb.SetRequest) (*pb.SetResponse, return s.Server.Set(ctx, req) } -// Set overrides the Subscribe func of gnmi.Target to provide user auth. +// Subscribe overrides the Subscribe func of gnmi.Target to provide user auth. func (s *server) Subscribe(stream pb.GNMI_SubscribeServer) error { msg, ok := credentials.AuthorizeUser(stream.Context()) if !ok { @@ -90,6 +93,24 @@ func (s *server) Subscribe(stream pb.GNMI_SubscribeServer) error { return s.Server.Subscribe(stream) } +// shutdownHook saves the running config back out to the config file. +func (s *server) shutdownHook (c chan os.Signal) { + sig := <- c + log.Infof("Gracefully stopping: %s", sig) + cfg, err := s.ConfigAsJSON() + if err != nil { + log.Exitf("Error getting json representation from server: %v", err) + } + if *configFile != "" { + err := os.WriteFile(*configFile, []byte(cfg), 0644) + if err != nil { + log.Exitf("error in writing config file: %v", err) + } + } + log.Infof("Config saved back out to file.") + os.Exit(0) +} + func main() { model := gnmi.NewModel(modeldata.ModelData, reflect.TypeOf((*gostruct.Device)(nil)), @@ -134,6 +155,12 @@ func main() { log.Exitf("failed to listen: %v", err) } + if(*saveOnExit) { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + go s.shutdownHook(c) + } + log.Info("starting to serve") if err := g.Serve(listen); err != nil { log.Exitf("failed to serve: %v", err)