Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yang library advertisement and yang file download #65

Merged
merged 2 commits into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 29 additions & 1 deletion rest/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 ""
}
24 changes: 24 additions & 0 deletions rest/server/restconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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("<yang-library-version xmlns='urn:ietf:params:xml:ns:yang:ietf-restconf'>")
data.WriteString("2016-06-21</yang-library-version>")
} 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) {
Expand Down
57 changes: 57 additions & 0 deletions rest/server/restconf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
43 changes: 28 additions & 15 deletions rest/server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"strings"
"time"

"github.com/Azure/sonic-mgmt-common/translib"
"github.com/golang/glog"
"github.com/gorilla/mux"
)
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down