Skip to content

Commit c0faf5c

Browse files
Implements the frontend logic for gNSI Pathz
1 parent 59fe88e commit c0faf5c

File tree

13 files changed

+2326
-33
lines changed

13 files changed

+2326
-33
lines changed

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ endif
113113

114114
# download and apply patch for gnmi client, which will break advancetls
115115
# backup crypto and gnxi
116-
mkdir backup_crypto
116+
mkdir -p backup_crypto
117117
cp -r vendor/golang.org/x/crypto/* backup_crypto/
118118

119119
# download and patch crypto and gnxi
@@ -126,6 +126,17 @@ endif
126126
git apply patches/0001-Updated-to-filter-and-write-to-file.patch
127127
git apply patches/0003-Fix-client-json-parsing-issue.patch
128128

129+
# Manually adding patched client packages and their dependencies
130+
# to vendor/modules.txt. This satisfies 'go install -mod=vendor' lookup checks,
131+
# which are required after manual patching/copying of gnxi and gnmi-cli code.
132+
echo "github.com/google/gnxi v0.0.0-20181220173256-89f51f0ce1e2" >> vendor/modules.txt
133+
echo "github.com/google/gnxi/gnmi_get" >> vendor/modules.txt
134+
echo "github.com/google/gnxi/gnmi_set" >> vendor/modules.txt
135+
echo "github.com/openconfig/gnmi/cli" >> vendor/modules.txt
136+
echo "github.com/openconfig/gnmi/client/flags" >> vendor/modules.txt
137+
echo "golang.org/x/crypto/ssh/terminal" >> vendor/modules.txt
138+
echo "github.com/openconfig/gnmi/cmd/gnmi_cli" >> vendor/modules.txt
139+
129140
ifeq ($(CROSS_BUILD_ENVIRON),y)
130141
$(GO) build -o ${GOBIN}/gnmi_get -mod=vendor github.com/google/gnxi/gnmi_get
131142
$(GO) build -o ${GOBIN}/gnmi_set -mod=vendor github.com/google/gnxi/gnmi_set
@@ -212,6 +223,7 @@ check_gotest: $(DBCONFG) $(ENVFILE)
212223
sudo CGO_LDFLAGS="$(CGO_LDFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" $(GO) test -race -coverprofile=coverage-telemetry.txt -covermode=atomic -mod=vendor -v github.com/sonic-net/sonic-gnmi/telemetry
213224
sudo CGO_LDFLAGS="$(CGO_LDFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" $(GO) test -race -coverprofile=coverage-config.txt -covermode=atomic -v github.com/sonic-net/sonic-gnmi/sonic_db_config
214225
sudo CGO_LDFLAGS="$(CGO_LDFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" $(TESTENV) $(GO) test -race -timeout 20m -coverprofile=coverage-gnmi.txt -covermode=atomic -mod=vendor $(BLD_FLAGS) -v github.com/sonic-net/sonic-gnmi/gnmi_server -coverpkg ../...
226+
sudo CGO_LDFLAGS="$(CGO_LDFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" $(TESTENV) $(GO) test -race -timeout 20m -coverprofile=coverage-pathz_authorizer.txt -covermode=atomic -mod=vendor $(BLD_FLAGS) -v github.com/sonic-net/sonic-gnmi/pathz_authorizer -coverpkg ../...
215227
ifneq ($(ENABLE_DIALOUT_VALUE),0)
216228
sudo CGO_LDFLAGS="$(CGO_LDFLAGS)" CGO_CXXFLAGS="$(CGO_CXXFLAGS)" $(TESTENV) $(GO) test -coverprofile=coverage-dialout.txt -covermode=atomic -mod=vendor $(BLD_FLAGS) -v github.com/sonic-net/sonic-gnmi/dialout/dialout_client
217229
endif

gnmi_server/gnsi_pathz.go

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
package gnmi
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"os"
10+
"strconv"
11+
"sync"
12+
13+
lvl "github.com/sonic-net/sonic-gnmi/gnmi_server/log"
14+
"github.com/sonic-net/sonic-gnmi/pathz_authorizer"
15+
16+
log "github.com/golang/glog"
17+
"github.com/golang/protobuf/proto"
18+
"github.com/openconfig/gnsi/pathz"
19+
"google.golang.org/grpc/codes"
20+
"google.golang.org/grpc/status"
21+
"path/filepath"
22+
)
23+
24+
var (
25+
pathzMu sync.Mutex
26+
)
27+
28+
const (
29+
pathzTbl string = "PATHZ_POLICY|"
30+
pathzVersionFld string = "pathz_version"
31+
pathzCreatedOnFld string = "pathz_created_on"
32+
pathzPolicyActive pathzInstance = "ACTIVE"
33+
// support for sandbox not yet implemented
34+
pathzPolicySandbox pathzInstance = "SANDBOX"
35+
)
36+
37+
type pathzInstance string
38+
type PathzMetadata struct {
39+
PathzVersion string `json:"pathz_version"`
40+
PathzCreatedOn string `json:"pathz_created_on"`
41+
}
42+
43+
type GNSIPathzServer struct {
44+
*Server
45+
pathzProcessor pathz_authorizer.GnmiAuthzProcessorInterface
46+
pathzMetadata *PathzMetadata
47+
pathzMetadataCopy *PathzMetadata
48+
policyCopy *pathz.AuthorizationPolicy
49+
policyUpdated bool
50+
pathzV1Policy string
51+
pathzV1PolicyBackup string
52+
pathz.UnimplementedPathzServer
53+
}
54+
55+
func NewPathzMetadata() *PathzMetadata {
56+
return &PathzMetadata{
57+
PathzVersion: "unknown",
58+
PathzCreatedOn: "0",
59+
}
60+
}
61+
62+
func (srv *GNSIPathzServer) Probe(context.Context, *pathz.ProbeRequest) (*pathz.ProbeResponse, error) {
63+
return nil, status.Errorf(codes.Unimplemented, "method Probe not implemented")
64+
}
65+
66+
func (srv *GNSIPathzServer) Get(context.Context, *pathz.GetRequest) (*pathz.GetResponse, error) {
67+
return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
68+
}
69+
70+
func NewGNSIPathzServer(srv *Server) *GNSIPathzServer {
71+
ret := &GNSIPathzServer{
72+
Server: srv,
73+
pathzProcessor: &pathz_authorizer.GnmiAuthzProcessor{},
74+
pathzMetadata: NewPathzMetadata(),
75+
pathzV1Policy: srv.config.PathzPolicyFile,
76+
pathzV1PolicyBackup: srv.config.PathzPolicyFile + ".backup",
77+
}
78+
if err := ret.loadPathzFreshness(srv.config.PathzMetaFile); err != nil {
79+
log.V(lvl.ERROR).Info(err)
80+
}
81+
ret.writePathzMetadataToDB(pathzPolicyActive)
82+
if srv.config.PathzPolicy {
83+
if err := ret.pathzProcessor.UpdatePolicyFromFile(ret.pathzV1Policy); err != nil {
84+
log.V(lvl.ERROR).Infof("Failed to load gNMI pathz file %s: %v", ret.pathzV1Policy, err)
85+
}
86+
}
87+
return ret
88+
}
89+
90+
func (srv *GNSIPathzServer) savePathzFileFreshess(path string) error {
91+
log.V(lvl.INFO).Infof("Saving pathz metadata to file: %s", path)
92+
buf := new(bytes.Buffer)
93+
enc := json.NewEncoder(buf)
94+
if err := enc.Encode(*srv.pathzMetadata); err != nil {
95+
log.V(lvl.ERROR).Info(err)
96+
return err
97+
}
98+
return attemptWrite(path, buf.Bytes(), 0o644)
99+
}
100+
101+
func (srv *GNSIPathzServer) loadPathzFreshness(path string) error {
102+
bytes, err := os.ReadFile(path)
103+
if err != nil {
104+
return err
105+
}
106+
return json.Unmarshal(bytes, srv.pathzMetadata)
107+
}
108+
109+
func (srv *GNSIPathzServer) savePathzPolicyToFile(p *pathz.AuthorizationPolicy) (string, error) {
110+
content := proto.MarshalTextString(p)
111+
log.V(lvl.DEBUG).Infof("Saving pathz policy to file: %s", srv.pathzV1Policy)
112+
return content, attemptWrite(srv.pathzV1Policy, []byte(content), 0o644)
113+
}
114+
115+
func (srv *GNSIPathzServer) verifyPathzFile(c string) error {
116+
content, err := os.ReadFile(srv.pathzV1Policy)
117+
if err != nil {
118+
return err
119+
}
120+
if c != string(content) {
121+
return fmt.Errorf("Pathz file %s contains error.", srv.pathzV1Policy)
122+
}
123+
return nil
124+
}
125+
126+
func (srv *GNSIPathzServer) writePathzMetadataToDB(instance pathzInstance) error {
127+
id := string(instance)
128+
log.V(lvl.INFO).Infof("Writing pathz metadata to DB: %s Version: %s CreatedOn: %s", id, srv.pathzMetadata.PathzVersion, srv.pathzMetadata.PathzCreatedOn)
129+
if err := writeCredentialsMetadataToDB(pathzTbl+id, "", pathzVersionFld, srv.pathzMetadata.PathzVersion); err != nil {
130+
return err
131+
}
132+
return writeCredentialsMetadataToDB(pathzTbl+id, "", pathzCreatedOnFld, srv.pathzMetadata.PathzCreatedOn)
133+
}
134+
135+
func (srv *GNSIPathzServer) updatePolicy(p *pathz.AuthorizationPolicy) error {
136+
log.V(lvl.INFO).Info("Updating gNMI pathz policy")
137+
log.V(lvl.DEBUG).Infof("Policy: %v", p.String())
138+
c, err := srv.savePathzPolicyToFile(p)
139+
if err != nil {
140+
return err
141+
}
142+
if err := srv.verifyPathzFile(c); err != nil {
143+
log.V(lvl.ERROR).Infof("Failed to verify gNMI pathz policy: %v", err)
144+
return err
145+
}
146+
err = srv.pathzProcessor.UpdatePolicyFromProto(p)
147+
if err != nil {
148+
log.V(lvl.ERROR).Infof("Failed to update gNMI pathz policy: %v", err)
149+
}
150+
return err
151+
}
152+
153+
func (srv *GNSIPathzServer) createCheckpoint() error {
154+
log.V(lvl.INFO).Info("Creating gNMI pathz policy checkpoint")
155+
srv.policyCopy = srv.pathzProcessor.GetPolicy()
156+
srv.policyUpdated = false
157+
srv.pathzMetadataCopy = srv.pathzMetadata
158+
return copyFile(srv.pathzV1Policy, srv.pathzV1PolicyBackup)
159+
}
160+
161+
func (srv *GNSIPathzServer) revertPolicy() error {
162+
log.V(lvl.INFO).Info("Reverting gNMI pathz policy")
163+
if srv.policyUpdated {
164+
srv.policyUpdated = false
165+
if err := srv.pathzProcessor.UpdatePolicyFromProto(srv.policyCopy); err != nil {
166+
log.V(lvl.ERROR).Infof("Failed to revert gNMI pathz policy: %v", err)
167+
os.Remove(srv.pathzV1PolicyBackup)
168+
return err
169+
}
170+
}
171+
srv.pathzMetadata = srv.pathzMetadataCopy
172+
return os.Rename(srv.pathzV1PolicyBackup, srv.pathzV1Policy)
173+
}
174+
175+
func (srv *GNSIPathzServer) commitChanges() error {
176+
log.V(lvl.INFO).Info("Committing gNMI pathz policy changes")
177+
if err := srv.writePathzMetadataToDB(pathzPolicyActive); err != nil {
178+
return err
179+
}
180+
return srv.savePathzFileFreshess(srv.config.PathzMetaFile)
181+
}
182+
183+
// Rotate implements the gNSI.pathz.Rotate RPC.
184+
func (srv *GNSIPathzServer) Rotate(stream pathz.Pathz_RotateServer) error {
185+
// Reject while NSF Freeze is ongoing
186+
log.V(lvl.INFO).Info("gNSI pathz Rotate RPC")
187+
// TODO(b/344081417) Enable code once NSF Freeze is implemented
188+
// Reject while NSF Freeze is ongoing
189+
// if srv.Server.WarmRestartHelper.FetchFreezeStatus() {
190+
// log.V(lvl.ERROR).Info("gNSI pathz Rotate RPC disabled since NSF is ongoing!")
191+
// return status.Errorf(codes.Unavailable, "gNSI pathz Rotate RPC disabled since NSF is ongoing!")
192+
// }
193+
ctx := stream.Context()
194+
ctx, err := authenticate(srv.config, ctx, "gnoi", false)
195+
if err != nil {
196+
return err
197+
}
198+
// Concurrent Pathz RPCs are not allowed.
199+
if !pathzMu.TryLock() {
200+
log.V(lvl.ERROR).Infoln("Concurrent Pathz RPCs are not allowed")
201+
return status.Errorf(codes.Aborted, "Concurrent Pathz RPCs are not allowed")
202+
}
203+
defer pathzMu.Unlock()
204+
if err := fileCheck(srv.pathzV1Policy); err != nil {
205+
log.V(lvl.ERROR).Infof("Error in reading file %s: %v", srv.pathzV1Policy, err)
206+
return status.Errorf(codes.NotFound, "Error in reading file %s: %v", srv.pathzV1Policy, err)
207+
}
208+
if err := srv.createCheckpoint(); err != nil {
209+
log.V(lvl.ERROR).Infof("Error in creating checkpoint: %v", err)
210+
return status.Errorf(codes.Aborted, "Error in creating checkpoint: %v", err)
211+
}
212+
for {
213+
req, err := stream.Recv()
214+
log.V(lvl.DEBUG).Infof("Received a Rotate request message: %v", req.String())
215+
if err == io.EOF {
216+
log.V(lvl.ERROR).Infoln("Received EOF instead of a UploadRequest/Finalize request! Reverting to last good state")
217+
// Connection closed without Finalize message. Revert all changes made until now.
218+
if err := srv.revertPolicy(); err != nil {
219+
return status.Errorf(codes.Aborted, "Error in reverting policy: %v", err)
220+
}
221+
return status.Errorf(codes.Aborted, "No Finalize message")
222+
}
223+
if err != nil {
224+
log.V(lvl.ERROR).Infof("Reverting to last good state Received error: %v", err)
225+
// Connection closed without Finalize message. Revert all changes made until now.
226+
srv.revertPolicy()
227+
return status.Errorf(codes.Aborted, err.Error())
228+
}
229+
if endReq := req.GetFinalizeRotation(); endReq != nil {
230+
// This is the last message. All changes are final.
231+
log.V(lvl.INFO).Infof("Received a Finalize request message: %v", endReq)
232+
if !srv.policyUpdated {
233+
log.V(lvl.ERROR).Infoln("Received finalize message without successful rotation")
234+
srv.revertPolicy()
235+
return status.Errorf(codes.Aborted, "Received finalize message without successful rotation")
236+
}
237+
if err := srv.commitChanges(); err != nil {
238+
// Revert won't be called if the final commit fails.
239+
return status.Errorf(codes.Aborted, "Final policy commit fails: %v", err)
240+
}
241+
os.Remove(srv.pathzV1PolicyBackup)
242+
return nil
243+
}
244+
resp, err := srv.processRotateRequest(req)
245+
if err != nil {
246+
log.V(lvl.ERROR).Infof("Reverting to last good state; While processing a rotate request got error: %v", err)
247+
// Connection closed without Finalize message. Revert all changes made until now.
248+
srv.revertPolicy()
249+
return err
250+
}
251+
if err := stream.Send(resp); err != nil {
252+
log.V(lvl.ERROR).Infof("Reverting to last good state; While sending a confirmation got error: %v", err)
253+
// Connection closed without Finalize message. Revert all changes made until now.
254+
srv.revertPolicy()
255+
return status.Errorf(codes.Aborted, err.Error())
256+
}
257+
}
258+
}
259+
260+
func (srv *GNSIPathzServer) processRotateRequest(req *pathz.RotateRequest) (*pathz.RotateResponse, error) {
261+
policyReq := req.GetUploadRequest()
262+
if policyReq == nil {
263+
return nil, status.Errorf(codes.Aborted, "Unknown request: %v", req)
264+
}
265+
log.V(lvl.INFO).Infof("Received a gNSI.Pathz UploadRequest request message")
266+
if len(policyReq.GetVersion()) == 0 {
267+
return nil, status.Errorf(codes.Aborted, "Pathz policy version cannot be empty")
268+
}
269+
if srv.pathzMetadata.PathzVersion == policyReq.GetVersion() && !req.GetForceOverwrite() {
270+
return nil, status.Errorf(codes.AlreadyExists, "Pathz with version `%v` already exists", policyReq.GetVersion())
271+
}
272+
srv.pathzMetadata.PathzVersion = policyReq.GetVersion()
273+
srv.pathzMetadata.PathzCreatedOn = strconv.FormatUint(policyReq.GetCreatedOn(), 10)
274+
if err := srv.updatePolicy(policyReq.GetPolicy()); err != nil {
275+
return nil, status.Errorf(codes.Aborted, err.Error())
276+
}
277+
srv.policyUpdated = true
278+
resp := &pathz.RotateResponse{
279+
Response: &pathz.RotateResponse_Upload{},
280+
}
281+
return resp, nil
282+
}
283+
func attemptWrite(name string, data []byte, perm os.FileMode) error {
284+
log.V(lvl.INFO).Infof("Writing: %s", name)
285+
err := os.WriteFile(name, data, perm)
286+
if err != nil {
287+
if e := os.Remove(name); e != nil {
288+
err = fmt.Errorf("Write %s failed: %w; Cleanup failed", name, err)
289+
}
290+
}
291+
return err
292+
}
293+
func fileCheck(f string) error {
294+
srcStat, err := os.Stat(f)
295+
if err != nil {
296+
return err
297+
}
298+
if !srcStat.Mode().IsRegular() {
299+
return fmt.Errorf("%s is not a regular file", f)
300+
}
301+
return nil
302+
}
303+
func copyFile(srcPath, dstPath string) error {
304+
srcStat, err := os.Stat(srcPath)
305+
if err != nil {
306+
return err
307+
}
308+
if !srcStat.Mode().IsRegular() {
309+
return fmt.Errorf("%s is not a regular file", srcPath)
310+
}
311+
src, err := os.Open(srcPath)
312+
if err != nil {
313+
return err
314+
}
315+
defer src.Close()
316+
tmpDst, err := os.CreateTemp(filepath.Dir(dstPath), filepath.Base(dstPath))
317+
if err != nil {
318+
return err
319+
}
320+
if _, err := io.Copy(tmpDst, src); err != nil {
321+
if e := os.Remove(tmpDst.Name()); e != nil {
322+
log.V(2).Infof("Failed to cleanup file: %v: %v", tmpDst.Name(), e)
323+
}
324+
return err
325+
}
326+
if err := tmpDst.Close(); err != nil {
327+
if e := os.Remove(tmpDst.Name()); e != nil {
328+
log.V(2).Infof("Failed to cleanup file: %v: %v", tmpDst.Name(), e)
329+
}
330+
return err
331+
}
332+
if err := os.Rename(tmpDst.Name(), dstPath); err != nil {
333+
if e := os.Remove(tmpDst.Name()); e != nil {
334+
log.V(2).Infof("Failed to cleanup file: %v: %v", tmpDst.Name(), e)
335+
}
336+
return err
337+
}
338+
return os.Chmod(dstPath, 0600)
339+
}

0 commit comments

Comments
 (0)