From ac892f19019984bef385864b381c0e46bef71fee Mon Sep 17 00:00:00 2001 From: Sachin Holla Date: Sat, 24 Oct 2020 18:43:30 +0530 Subject: [PATCH 1/2] Yang library version advertisement Enhanced REST server to handle "GET /restconf/yang-library-version" request to advertise yang library version (RFC8040, section 3.3.3). Always returns "2016-06-21". --- rest/server/restconf.go | 24 +++++++++++++++ rest/server/restconf_test.go | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/rest/server/restconf.go b/rest/server/restconf.go index 070805ca33..9f7e8a925b 100644 --- a/rest/server/restconf.go +++ b/rest/server/restconf.go @@ -45,6 +45,9 @@ func init() { // Metadata discovery handler AddRoute("hostMetadataHandler", "GET", "/.well-known/host-meta", hostMetadataHandler) + // yanglib version handler + AddRoute("yanglibVersionHandler", "GET", "/restconf/yang-library-version", yanglibVersionHandler) + // RESTCONF capability handler AddRoute("capabilityHandler", "GET", "/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities", capabilityHandler) @@ -66,6 +69,27 @@ func hostMetadataHandler(w http.ResponseWriter, r *http.Request) { w.Write(data.Bytes()) } +// yanglibVersionHandler handles "GET /restconf/yang-library-version" +// request as per RFC8040. Yanglib version supported is "2016-06-21" +func yanglibVersionHandler(w http.ResponseWriter, r *http.Request) { + var data bytes.Buffer + var contentType string + accept := r.Header.Get("Accept") + + // Rudimentary content negotiation + if strings.Contains(accept, mimeYangDataXML) { + contentType = mimeYangDataXML + data.WriteString("") + data.WriteString("2016-06-21") + } else { + contentType = mimeYangDataJSON + data.WriteString("{\"ietf-restconf:yang-library-version\": \"2016-06-21\"}") + } + + w.Header().Set("Content-Type", contentType) + w.Write(data.Bytes()) +} + // capabilityHandler serves RESTCONF capability requests - // "GET /restconf/data/ietf-restconf-monitoring:restconf-state/capabilities" func capabilityHandler(w http.ResponseWriter, r *http.Request) { diff --git a/rest/server/restconf_test.go b/rest/server/restconf_test.go index 259a7ca20b..48f35ca508 100644 --- a/rest/server/restconf_test.go +++ b/rest/server/restconf_test.go @@ -78,6 +78,63 @@ func TestMetaHandler(t *testing.T) { } } +func TestYanglibVer_json(t *testing.T) { + testYanglibVer(t, mimeYangDataJSON, mimeYangDataJSON) +} + +func TestYanglibVer_xml(t *testing.T) { + testYanglibVer(t, mimeYangDataXML, mimeYangDataXML) +} + +func TestYanglibVer_default(t *testing.T) { + testYanglibVer(t, "", mimeYangDataJSON) +} + +func TestYanglibVer_unknown(t *testing.T) { + testYanglibVer(t, "text/plain", mimeYangDataJSON) +} + +func testYanglibVer(t *testing.T, requestAcceptType, expectedContentType string) { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/restconf/yang-library-version", nil) + if requestAcceptType != "" { + r.Header.Set("Accept", requestAcceptType) + } + + t.Logf("GET /restconf/yang-library-version with accept=%s", requestAcceptType) + newDefaultRouter().ServeHTTP(w, r) + + if w.Code != 200 { + t.Fatalf("Request failed with status %d", w.Code) + } + if len(w.Body.Bytes()) == 0 { + t.Fatalf("No response body") + } + if w.Header().Get("Content-Type") != expectedContentType { + t.Fatalf("Expected content-type=%s, found=%s", expectedContentType, w.Header().Get("Content-Type")) + } + + var err error + var resp struct { + XMLName xml.Name `json:"-" xml:"urn:ietf:params:xml:ns:yang:ietf-restconf yang-library-version"` + Version string `json:"ietf-restconf:yang-library-version" xml:",chardata"` + } + + if expectedContentType == mimeYangDataXML { + err = xml.Unmarshal(w.Body.Bytes(), &resp) + } else { + err = json.Unmarshal(w.Body.Bytes(), &resp) + } + if err != nil { + t.Fatalf("Response parsing failed; err=%v", err) + } + + t.Logf("GOT yang-library-version %s; content-type=%s", resp.Version, w.Header().Get("Content-Type")) + if resp.Version != "2016-06-21" { + t.Fatalf("Expected yanglib version 2016-06-21; received=%s", resp.Version) + } +} + func TestCapability_1(t *testing.T) { testCapability(t, "/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities") } From 3f606ffd1e28020fedbe8ceaee5bcf1b50852806 Mon Sep 17 00:00:00 2001 From: Sachin Holla Date: Sat, 24 Oct 2020 22:39:07 +0530 Subject: [PATCH 2/2] Yang file download REST server now supports "GET /models/yang/xxxxx.yang" requests to download yang files from the device. These links are also advertised through schema URLs in the ietf-yang-library responses. Schema URL uses an ipv4 address of eth0. Requires restart of REST server to reflect changes to eth0 ip address. --- rest/main/main.go | 30 +++++++++++++++++++++++++++++- rest/server/router.go | 43 ++++++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/rest/main/main.go b/rest/main/main.go index bb48516f0d..8046dbcdbb 100644 --- a/rest/main/main.go +++ b/rest/main/main.go @@ -25,13 +25,14 @@ import ( "flag" "fmt" "io/ioutil" + "net" "net/http" "os" "os/signal" "syscall" - "github.com/Azure/sonic-mgmt-framework/rest/server" "github.com/Azure/sonic-mgmt-framework/build/rest_server/dist/swagger" + "github.com/Azure/sonic-mgmt-framework/rest/server" "github.com/golang/glog" "github.com/pkg/profile" ) @@ -82,6 +83,9 @@ func main() { if clientAuth == "user" { rtrConfig.AuthEnable = true } + if ip := findAManagementIP(); ip != "" { + rtrConfig.ServerAddr = fmt.Sprintf("https://%s:%d", ip, port) + } router := server.NewRouter(rtrConfig) @@ -190,3 +194,27 @@ func getPreferredCipherSuites() []uint16 { tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, } } + +// findAManagementIP returns a valid IPv4 address of eth0. +// Empty string is returned if no address could be resolved. +func findAManagementIP() string { + var addrs []net.Addr + eth0, err := net.InterfaceByName("eth0") + if err == nil { + addrs, err = eth0.Addrs() + } + if err != nil { + glog.Errorf("Could not read eth0 info; err=%v", err) + return "" + } + + for _, addr := range addrs { + ip, _, err := net.ParseCIDR(addr.String()) + if err == nil && ip.To4() != nil { + return ip.String() + } + } + + glog.Warning("Could not find a management address!!") + return "" +} diff --git a/rest/server/router.go b/rest/server/router.go index 36d3416951..77263e2b9a 100644 --- a/rest/server/router.go +++ b/rest/server/router.go @@ -27,6 +27,7 @@ import ( "strings" "time" + "github.com/Azure/sonic-mgmt-common/translib" "github.com/golang/glog" "github.com/gorilla/mux" ) @@ -51,6 +52,10 @@ type Router struct { type RouterConfig struct { // AuthEnable indicates if client authentication is enabled AuthEnable bool + + // ServerAddr is the address to contact main server. Will be used to + // advertise the server's address (like yang download path).. Optional + ServerAddr string } // ServeHTTP resolves and invokes the handler for http request r. @@ -298,7 +303,7 @@ func NewRouter(config RouterConfig) *Router { allRoutes.rcRouteCount, allRoutes.muxRouteCount) // Add internal service API routes if not added already - allRoutes.addServiceRoutes() + allRoutes.addServiceRoutes(&config) router := &Router{ config: config, @@ -322,8 +327,6 @@ type routeStore struct { muxOptsHandler http.Handler // OPTIONS handler for mux routes muxOptsData map[string][]string // path to operations map for mux routes muxRouteCount uint32 // number of routes in mux router - - svcRoutesAdded bool // indicates if service routes have been registered } // newRouteStore creates an empty routeStore instance. @@ -363,23 +366,33 @@ func (rs *routeStore) addMuxRoute(rr *routeRegInfo) { rs.muxRouteCount++ } -// finish creates routes for all internal service API handlers in +// addServiceRoutes creates routes for all internal service API handlers in // mux router. Should be called after all REST API routes are added. -func (rs *routeStore) addServiceRoutes() { - if rs.svcRoutesAdded { - return - } - - rs.svcRoutesAdded = true +func (rs *routeStore) addServiceRoutes(config *RouterConfig) { router := rs.muxRoutes // Documentation and test UI - uiHandler := http.StripPrefix("/ui/", http.FileServer(http.Dir(swaggerUIDir))) - router.Methods("GET").PathPrefix("/ui/").Handler(uiHandler) + if router.Get("swaggerUI") == nil { + rs.addFilesystemRoute("swaggerUI", "/ui/", swaggerUIDir) + + // Redirect "/ui" to "/ui/index.html" + router.Methods("GET").Path("/ui"). + Handler(http.RedirectHandler("/ui/index.html", http.StatusMovedPermanently)) + } + + // Yang download + if config.ServerAddr != "" && router.Get("yangDownload") == nil { + yangPrefix := "/models/yang/" + translib.SetSchemaRootURL(strings.TrimSuffix(config.ServerAddr, "/") + yangPrefix) + rs.addFilesystemRoute("yangDownload", yangPrefix, translib.GetYangPath()) + } +} - // Redirect "/ui" to "/ui/index.html" - router.Methods("GET").Path("/ui"). - Handler(http.RedirectHandler("/ui/index.html", http.StatusMovedPermanently)) +// addFilesystemRoute creates a mux route to handle file system based GET requests. +func (rs *routeStore) addFilesystemRoute(name, prefix, dir string) { + h := http.StripPrefix(prefix, http.FileServer(http.Dir(dir))) + //TODO enable authentication? + rs.muxRoutes.Name(name).Methods("GET", "HEAD").PathPrefix(prefix).Handler(h) } // getRouteMatchInfo returns routeMatchInfo from request context.