Browse Source

Merge branch 'netclient_refactor_latest' of https://github.com/gravitl/netmaker into netclient_refactor_peer_updates

Abhishek Kondur 2 years ago
parent
commit
3f8f841313
100 changed files with 3190 additions and 84 deletions
  1. 1 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  2. 1 2
      .github/workflows/buildandrelease.yml
  3. 18 14
      .github/workflows/publish-docker.yml
  4. 23 6
      .github/workflows/test.yml
  5. 2 0
      .gitignore
  6. 1 1
      Dockerfile
  7. 2 2
      README.md
  8. 108 25
      auth/auth.go
  9. 4 7
      auth/azure-ad.go
  10. 9 0
      auth/error.go
  11. 4 7
      auth/github.go
  12. 4 7
      auth/google.go
  13. 93 0
      auth/headless_callback.go
  14. 5 4
      auth/nodecallback.go
  15. 1 2
      auth/nodesession.go
  16. 4 7
      auth/oidc.go
  17. 46 0
      cli/cmd/acl/allow.go
  18. 46 0
      cli/cmd/acl/deny.go
  19. 46 0
      cli/cmd/acl/list.go
  20. 38 0
      cli/cmd/acl/root.go
  21. 20 0
      cli/cmd/context/delete.go
  22. 20 0
      cli/cmd/context/list.go
  23. 38 0
      cli/cmd/context/root.go
  24. 48 0
      cli/cmd/context/set.go
  25. 20 0
      cli/cmd/context/use.go
  26. 33 0
      cli/cmd/dns/create.go
  27. 20 0
      cli/cmd/dns/delete.go
  28. 9 0
      cli/cmd/dns/flags.go
  29. 47 0
      cli/cmd/dns/list.go
  30. 22 0
      cli/cmd/dns/push.go
  31. 38 0
      cli/cmd/dns/root.go
  32. 22 0
      cli/cmd/ext_client/config.go
  33. 26 0
      cli/cmd/ext_client/create.go
  34. 20 0
      cli/cmd/ext_client/delete.go
  35. 20 0
      cli/cmd/ext_client/get.go
  36. 40 0
      cli/cmd/ext_client/list.go
  37. 38 0
      cli/cmd/ext_client/root.go
  38. 71 0
      cli/cmd/ext_client/update.go
  39. 35 0
      cli/cmd/keys/create.go
  40. 23 0
      cli/cmd/keys/delete.go
  41. 20 0
      cli/cmd/keys/list.go
  42. 38 0
      cli/cmd/keys/root.go
  43. 22 0
      cli/cmd/logs.go
  44. 20 0
      cli/cmd/metrics/all.go
  45. 20 0
      cli/cmd/metrics/network.go
  46. 20 0
      cli/cmd/metrics/network_ext.go
  47. 20 0
      cli/cmd/metrics/node.go
  48. 38 0
      cli/cmd/metrics/root.go
  49. 85 0
      cli/cmd/network/create.go
  50. 22 0
      cli/cmd/network/delete.go
  51. 22 0
      cli/cmd/network/flags.go
  52. 20 0
      cli/cmd/network/get.go
  53. 32 0
      cli/cmd/network/list.go
  54. 27 0
      cli/cmd/network/node_limit.go
  55. 20 0
      cli/cmd/network/refresh_keys.go
  56. 40 0
      cli/cmd/network/root.go
  57. 86 0
      cli/cmd/network/update.go
  58. 43 0
      cli/cmd/network_user/create.go
  59. 23 0
      cli/cmd/network_user/delete.go
  60. 10 0
      cli/cmd/network_user/flags.go
  61. 27 0
      cli/cmd/network_user/get.go
  62. 27 0
      cli/cmd/network_user/list.go
  63. 38 0
      cli/cmd/network_user/root.go
  64. 43 0
      cli/cmd/network_user/update.go
  65. 34 0
      cli/cmd/node/create_egress.go
  66. 21 0
      cli/cmd/node/create_ingress.go
  67. 22 0
      cli/cmd/node/create_relay.go
  68. 20 0
      cli/cmd/node/delete.go
  69. 20 0
      cli/cmd/node/delete_egress.go
  70. 20 0
      cli/cmd/node/delete_ingress.go
  71. 20 0
      cli/cmd/node/delete_relay.go
  72. 28 0
      cli/cmd/node/flags.go
  73. 20 0
      cli/cmd/node/get.go
  74. 47 0
      cli/cmd/node/list.go
  75. 38 0
      cli/cmd/node/root.go
  76. 22 0
      cli/cmd/node/uncordon.go
  77. 100 0
      cli/cmd/node/update.go
  78. 69 0
      cli/cmd/root.go
  79. 20 0
      cli/cmd/server/config.go
  80. 22 0
      cli/cmd/server/has_admin.go
  81. 20 0
      cli/cmd/server/health.go
  82. 20 0
      cli/cmd/server/info.go
  83. 38 0
      cli/cmd/server/root.go
  84. 37 0
      cli/cmd/user/create.go
  85. 20 0
      cli/cmd/user/delete.go
  86. 9 0
      cli/cmd/user/flags.go
  87. 20 0
      cli/cmd/user/get.go
  88. 30 0
      cli/cmd/user/list.go
  89. 38 0
      cli/cmd/user/root.go
  90. 35 0
      cli/cmd/user/update.go
  91. 23 0
      cli/cmd/usergroup/create.go
  92. 23 0
      cli/cmd/usergroup/delete.go
  93. 20 0
      cli/cmd/usergroup/get.go
  94. 38 0
      cli/cmd/usergroup/root.go
  95. 145 0
      cli/config/config.go
  96. 18 0
      cli/functions/acl.go
  97. 43 0
      cli/functions/dns.go
  98. 49 0
      cli/functions/ext_client.go
  99. 189 0
      cli/functions/http_client.go
  100. 23 0
      cli/functions/keys.go

+ 1 - 0
.github/ISSUE_TEMPLATE/bug-report.yml

@@ -31,6 +31,7 @@ body:
       label: Version
       description: What version are you running?
       options:
+        - v0.17.1      
         - v0.17.0
         - v0.16.3
         - v0.16.2

+ 1 - 2
.github/workflows/buildandrelease.yml

@@ -55,7 +55,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v3
       - name: Setup go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.19
       - name: Build
@@ -71,4 +71,3 @@ jobs:
           prerelease: true
           asset_name: netmaker
 
-

+ 18 - 14
.github/workflows/publish-docker.yml

@@ -13,7 +13,7 @@ jobs:
   docker:
     runs-on: ubuntu-latest
     steps:
-      - 
+      -
         name: Set tag
         run: |
             if [[ -n "${{ github.event.inputs.tag }}" ]]; then
@@ -24,16 +24,16 @@ jobs:
               TAG="${{ github.ref_name }}"
             fi
             echo "TAG=${TAG}" >> $GITHUB_ENV
-      - 
+      -
         name: Checkout
         uses: actions/checkout@v3
-      - 
+      -
         name: Set up QEMU
         uses: docker/setup-qemu-action@v2
-      - 
+      -
         name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v2
-      - 
+      -
         name: Login to DockerHub
         uses: docker/login-action@v2
         with:
@@ -47,31 +47,33 @@ jobs:
           platforms: linux/amd64, linux/arm64, linux/arm/v7
           push: true
           tags: ${{ github.repository }}:${{ env.TAG }}, ${{ github.repository }}:latest
-          build-args: version=${{ env.TAG }}
+          build-args: | 
+            version=${{ env.TAG }}
+            tags=ce
 
   docker-ee:
     runs-on: ubuntu-latest
     steps:
-      - 
+      -
         name: Set tag
         run: |
             if [[ -n "${{ github.event.inputs.tag }}" ]]; then
-              docker/build-push-action@v3.tag }}
+              TAG=${{ github.event.inputs.tag }}
             elif [[ "${{ github.ref_name }}" == 'master' ]]; then
               TAG="latest"
             else
               TAG="${{ github.ref_name }}"
             fi
             echo "TAG=${TAG}" >> $GITHUB_ENV
-      - 
+      -
         name: Checkout
         uses: actions/checkout@v3
-      - 
+      -
         name: Set up QEMU
         uses: docker/setup-qemu-action@v2
-      - 
+      -
         name: Set up Docker Buildx
-        uses: docker/build-push-action@v3
+        uses: docker/setup-buildx-action@v2
       - 
         name: Login to DockerHub
         uses: docker/login-action@v2
@@ -83,7 +85,9 @@ jobs:
         uses: docker/build-push-action@v3
         with:
           context: .
-          platforms: linux/amd64, linux/arm64, linux/arm/v7
+          platforms: linux/amd64, linux/arm64
           push: true
           tags: ${{ github.repository }}:${{ env.TAG }}-ee
-          build-args: version=${{ env.TAG }}, tags="-tags=ee"
+          build-args: |
+            version=${{ env.TAG }}
+            tags=ee

+ 23 - 6
.github/workflows/test.yml

@@ -1,8 +1,9 @@
 name: Integration Test
 
 on:
+  workflow_dispatch:
   pull_request:
-    types: [opened, reopened]
+    types: [opened, synchronize, reopened]
 
 jobs:
   build:
@@ -11,7 +12,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v3
       - name: Setup Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.19
       - name: Build
@@ -23,13 +24,29 @@ jobs:
          env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build main.go
          env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go
          env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
+  nmctl:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Setup go
+        uses: actions/setup-go@v3
+        with:
+          go-version: 1.19
+      - name: Build
+        run: |
+          cd cli
+          GOOS=linux GOARCH=amd64 go build -o nmctl
+          GOOS=darwin GOARCH=amd64 go build -o nmctl
+          GOOS=darwin GOARCH=arm64 go build -o nmctl
+          GOOS=windows GOARCH=amd64 go build -o nmctl
   linux-gui:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
         uses: actions/checkout@v3
       - name: Setup Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.19
       - name: Build
@@ -43,7 +60,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v3
       - name: Setup Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.19
       - name: Build mac
@@ -55,7 +72,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v3
       - name: Setup Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.19
       - name: Mysys2 setup
@@ -75,7 +92,7 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v3
       - name: Setup Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.19
       - name: run tests

+ 2 - 0
.gitignore

@@ -3,6 +3,7 @@ netmaker-arm
 netmaker-arm64
 netmaker-32
 netmaker-amd64
+cli/nmctl
 netclient/netclient
 netclient/netclient.syso
 netclient/build
@@ -22,3 +23,4 @@ data/
 .vscode/
 .idea/
 netmaker.exe
+netmaker.code-workspace

+ 1 - 1
Dockerfile

@@ -7,7 +7,7 @@ COPY . .
 ENV GO111MODULE=auto
 
 RUN apk add git
-RUN GOOS=linux CGO_ENABLED=1 go build ${tags} -ldflags="-s -X 'main.version=${version}'" .
+RUN GOOS=linux CGO_ENABLED=1 go build -ldflags="-s -X 'main.version=${version}'" -tags ${tags} .
 # RUN go build -tags=ee . -o netmaker main.go
 FROM alpine:3.16.2
 

+ 2 - 2
README.md

@@ -17,7 +17,7 @@
 
 <p align="center">
   <a href="https://github.com/gravitl/netmaker/releases">
-    <img src="https://img.shields.io/badge/Version-0.17.0-informational?style=flat-square" />
+    <img src="https://img.shields.io/badge/Version-0.17.1-informational?style=flat-square" />
   </a>
   <a href="https://hub.docker.com/r/gravitl/netmaker/tags">
     <img src="https://img.shields.io/docker/pulls/gravitl/netmaker?label=downloads" />
@@ -57,7 +57,7 @@
 3. (optional) Prepare DNS - Set a wildcard subdomain in your DNS for Netmaker, e.g. *.netmaker.example.com
 4. Run the script: 
 
-`sudo wget -qO /root/nm-quick-interactive.sh https://raw.githubusercontent.com/gravitl/netmaker/test_v0.17.0_compose/scripts/nm-quick-interactive.sh && sudo chmod +x /root/nm-quick-interactive.sh && sudo /root/nm-quick-interactive.sh`  
+`sudo wget -qO /root/nm-quick-interactive.sh https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/nm-quick-interactive.sh && sudo chmod +x /root/nm-quick-interactive.sh && sudo /root/nm-quick-interactive.sh`  
 
 This script gives you the option to deploy the Community or Enterprise version of Netmaker. If deploying Enterprise, you get a free account with a 50 node limit by default. It also gives you the option to use your own domain (recommended) or an auto-generated domain. 
 

+ 108 - 25
auth/auth.go

@@ -3,17 +3,21 @@ package auth
 import (
 	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
+	"time"
 
+	"golang.org/x/crypto/bcrypt"
+	"golang.org/x/oauth2"
+
+	"github.com/gorilla/websocket"
 	"github.com/gravitl/netmaker/logger"
 	"github.com/gravitl/netmaker/logic"
 	"github.com/gravitl/netmaker/logic/pro/netcache"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/servercfg"
-	"golang.org/x/crypto/bcrypt"
-	"golang.org/x/oauth2"
 )
 
 // == consts ==
@@ -30,6 +34,7 @@ const (
 	auth_key               = "netmaker_auth"
 	user_signin_length     = 16
 	node_signin_length     = 64
+	headless_signin_length = 32
 )
 
 // OAuthUser - generic OAuth strategy user
@@ -41,7 +46,10 @@ type OAuthUser struct {
 	AccessToken       string `json:"accesstoken" bson:"accesstoken"`
 }
 
-var auth_provider *oauth2.Config
+var (
+	auth_provider *oauth2.Config
+	upgrader      = websocket.Upgrader{}
+)
 
 func getCurrentAuthFunctions() map[string]interface{} {
 	var authInfo = servercfg.GetAuthProviderInfo()
@@ -71,10 +79,6 @@ func InitializeAuthProvider() string {
 		logger.Log(0, err.Error())
 		return ""
 	}
-	var currentFrontendURL = servercfg.GetFrontendURL()
-	if currentFrontendURL == "" {
-		return ""
-	}
 	var authInfo = servercfg.GetAuthProviderInfo()
 	var serverConn = servercfg.GetAPIHost()
 	if strings.Contains(serverConn, "localhost") || strings.Contains(serverConn, "127.0.0.1") {
@@ -94,12 +98,11 @@ func InitializeAuthProvider() string {
 	return authInfo[0]
 }
 
-// Not included in API reference as part of the OAuth process itself.
 // HandleAuthCallback - handles oauth callback
+// Note: not included in API reference as part of the OAuth process itself.
 func HandleAuthCallback(w http.ResponseWriter, r *http.Request) {
 	if auth_provider == nil {
-		w.Header().Set("Content-Type", "text/html; charset=utf-8")
-		fmt.Fprintln(w, oauthNotConfigured)
+		handleOauthNotConfigured(w)
 		return
 	}
 	var functions = getCurrentAuthFunctions()
@@ -108,9 +111,17 @@ func HandleAuthCallback(w http.ResponseWriter, r *http.Request) {
 	}
 	state, _ := getStateAndCode(r)
 	_, err := netcache.Get(state) // if in netcache proceeed with node registration login
-	if err == nil || len(state) == node_signin_length || (err != nil && strings.Contains(err.Error(), "expired")) {
-		logger.Log(0, "proceeding with node SSO callback")
-		HandleNodeSSOCallback(w, r)
+	if err == nil || errors.Is(err, netcache.ErrExpired) {
+		switch len(state) {
+		case node_signin_length:
+			logger.Log(0, "proceeding with node SSO callback")
+			HandleNodeSSOCallback(w, r)
+		case headless_signin_length:
+			logger.Log(0, "proceeding with headless SSO callback")
+			HandleHeadlessSSOCallback(w, r)
+		default:
+			logger.Log(1, "invalid state length: ", fmt.Sprintf("%d", len(state)))
+		}
 	} else { // handle normal login
 		functions[handle_callback].(func(http.ResponseWriter, *http.Request))(w, r)
 	}
@@ -120,25 +131,23 @@ func HandleAuthCallback(w http.ResponseWriter, r *http.Request) {
 //
 // Handles OAuth login.
 //
-//		Schemes: https
+//			Schemes: https
 //
-// 		Security:
-//   		oauth
+//			Security:
+//	  		oauth
 func HandleAuthLogin(w http.ResponseWriter, r *http.Request) {
 	if auth_provider == nil {
-		var referer = r.Header.Get("referer")
-		if referer != "" {
-			http.Redirect(w, r, referer+"login?oauth=callback-error", http.StatusTemporaryRedirect)
-			return
-		}
-		w.Header().Set("Content-Type", "text/html; charset=utf-8")
-		fmt.Fprintln(w, oauthNotConfigured)
+		handleOauthNotConfigured(w)
 		return
 	}
 	var functions = getCurrentAuthFunctions()
 	if functions == nil {
 		return
 	}
+	if servercfg.GetFrontendURL() == "" {
+		handleOauthNotConfigured(w)
+		return
+	}
 	functions[handle_login].(func(http.ResponseWriter, *http.Request))(w, r)
 }
 
@@ -152,6 +161,80 @@ func IsOauthUser(user *models.User) error {
 	return bCryptErr
 }
 
+// HandleHeadlessSSO - handles the OAuth login flow for headless interfaces such as Netmaker CLI via websocket
+func HandleHeadlessSSO(w http.ResponseWriter, r *http.Request) {
+	conn, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		logger.Log(0, "error during connection upgrade for headless sign-in:", err.Error())
+		return
+	}
+	if conn == nil {
+		logger.Log(0, "failed to establish web-socket connection during headless sign-in")
+		return
+	}
+	defer conn.Close()
+
+	req := &netcache.CValue{User: "", Pass: ""}
+	stateStr := logic.RandomString(headless_signin_length)
+	if err = netcache.Set(stateStr, req); err != nil {
+		logger.Log(0, "Failed to process sso request -", err.Error())
+		return
+	}
+
+	timeout := make(chan bool, 1)
+	answer := make(chan string, 1)
+	defer close(answer)
+	defer close(timeout)
+
+	if auth_provider == nil {
+		if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
+			logger.Log(0, "error during message writing:", err.Error())
+		}
+		return
+	}
+	redirectUrl = fmt.Sprintf("https://%s/api/oauth/register/%s", servercfg.GetAPIConnString(), stateStr)
+	if err = conn.WriteMessage(websocket.TextMessage, []byte(redirectUrl)); err != nil {
+		logger.Log(0, "error during message writing:", err.Error())
+	}
+
+	go func() {
+		for {
+			cachedReq, err := netcache.Get(stateStr)
+			if err != nil {
+				if strings.Contains(err.Error(), "expired") {
+					logger.Log(0, "timeout occurred while waiting for SSO")
+					timeout <- true
+					break
+				}
+				continue
+			} else if cachedReq.Pass != "" {
+				logger.Log(0, "SSO process completed for user ", cachedReq.User)
+				answer <- cachedReq.Pass
+				break
+			}
+			time.Sleep(500) // try it 2 times per second to see if auth is completed
+		}
+	}()
+
+	select {
+	case result := <-answer:
+		if err = conn.WriteMessage(websocket.TextMessage, []byte(result)); err != nil {
+			logger.Log(0, "Error during message writing:", err.Error())
+		}
+	case <-timeout:
+		logger.Log(0, "Authentication server time out for headless SSO login")
+		if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
+			logger.Log(0, "Error during message writing:", err.Error())
+		}
+	}
+	if err = netcache.Del(stateStr); err != nil {
+		logger.Log(0, "failed to remove SSO cache entry", err.Error())
+	}
+	if err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil {
+		logger.Log(0, "write close:", err.Error())
+	}
+}
+
 // == private methods ==
 
 func addUser(email string) error {
@@ -169,7 +252,7 @@ func addUser(email string) error {
 		Password: newPass,
 	}
 	if !hasAdmin { // must be first attempt, create an admin
-		if newUser, err = logic.CreateAdmin(newUser); err != nil {
+		if err = logic.CreateAdmin(&newUser); err != nil {
 			logger.Log(1, "error creating admin from user,", email, "; user not added")
 		} else {
 			logger.Log(1, "admin created from user,", email, "; was first user added")
@@ -177,7 +260,7 @@ func addUser(email string) error {
 	} else { // otherwise add to db as admin..?
 		// TODO: add ability to add users with preemptive permissions
 		newUser.IsAdmin = false
-		if newUser, err = logic.CreateUser(newUser); err != nil {
+		if err = logic.CreateUser(&newUser); err != nil {
 			logger.Log(1, "error creating user,", email, "; user not added")
 		} else {
 			logger.Log(0, "user created from ", email)

+ 4 - 7
auth/azure-ad.go

@@ -37,16 +37,13 @@ func initAzureAD(redirectURL string, clientID string, clientSecret string) {
 
 func handleAzureLogin(w http.ResponseWriter, r *http.Request) {
 	var oauth_state_string = logic.RandomString(user_signin_length)
-	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
-		return
-	} else if auth_provider == nil {
-		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
+	if auth_provider == nil {
+		handleOauthNotConfigured(w)
 		return
 	}
 
 	if err := logic.SetState(oauth_state_string); err != nil {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		handleOauthNotConfigured(w)
 		return
 	}
 
@@ -60,7 +57,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getAzureUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from azure:", err.Error())
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		handleOauthNotConfigured(w)
 		return
 	}
 	_, err = logic.GetUser(content.UserPrincipalName)

+ 9 - 0
auth/error.go

@@ -1,5 +1,7 @@
 package auth
 
+import "net/http"
+
 // == define error HTML here ==
 const oauthNotConfigured = `<!DOCTYPE html><html>
 <body>
@@ -7,3 +9,10 @@ const oauthNotConfigured = `<!DOCTYPE html><html>
 <p>Please visit the docs <a href="https://docs.netmaker.org/oauth.html" target="_blank" rel="noopener">here</a> to learn how to.</p>
 </body>
 </html>`
+
+// handleOauthNotConfigured - returns an appropriate html page when oauth is not configured on netmaker server but an oauth login was attempted
+func handleOauthNotConfigured(response http.ResponseWriter) {
+	response.Header().Set("Content-Type", "text/html; charset=utf-8")
+	response.WriteHeader(http.StatusInternalServerError)
+	response.Write([]byte(oauthNotConfigured))
+}

+ 4 - 7
auth/github.go

@@ -37,16 +37,13 @@ func initGithub(redirectURL string, clientID string, clientSecret string) {
 
 func handleGithubLogin(w http.ResponseWriter, r *http.Request) {
 	var oauth_state_string = logic.RandomString(user_signin_length)
-	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
-		return
-	} else if auth_provider == nil {
-		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
+	if auth_provider == nil {
+		handleOauthNotConfigured(w)
 		return
 	}
 
 	if err := logic.SetState(oauth_state_string); err != nil {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		handleOauthNotConfigured(w)
 		return
 	}
 
@@ -60,7 +57,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getGithubUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from github:", err.Error())
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		handleOauthNotConfigured(w)
 		return
 	}
 	_, err = logic.GetUser(content.Login)

+ 4 - 7
auth/google.go

@@ -38,16 +38,13 @@ func initGoogle(redirectURL string, clientID string, clientSecret string) {
 
 func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
 	var oauth_state_string = logic.RandomString(user_signin_length)
-	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
-		return
-	} else if auth_provider == nil {
-		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
+	if auth_provider == nil {
+		handleOauthNotConfigured(w)
 		return
 	}
 
 	if err := logic.SetState(oauth_state_string); err != nil {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		handleOauthNotConfigured(w)
 		return
 	}
 
@@ -62,7 +59,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getGoogleUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from google:", err.Error())
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		handleOauthNotConfigured(w)
 		return
 	}
 	_, err = logic.GetUser(content.Email)

+ 93 - 0
auth/headless_callback.go

@@ -0,0 +1,93 @@
+package auth
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/logic"
+	"github.com/gravitl/netmaker/logic/pro/netcache"
+	"github.com/gravitl/netmaker/models"
+)
+
+// HandleHeadlessSSOCallback - handle OAuth callback for headless logins such as Netmaker CLI
+func HandleHeadlessSSOCallback(w http.ResponseWriter, r *http.Request) {
+	functions := getCurrentAuthFunctions()
+	if functions == nil {
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write([]byte("bad conf"))
+		logger.Log(0, "Missing Oauth config in HandleHeadlessSSOCallback")
+		return
+	}
+	state, code := getStateAndCode(r)
+
+	userClaims, err := functions[get_user_info].(func(string, string) (*OAuthUser, error))(state, code)
+	if err != nil {
+		logger.Log(0, "error when getting user info from callback:", err.Error())
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write([]byte("Failed to retrieve OAuth user claims"))
+		return
+	}
+
+	if code == "" || state == "" {
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write([]byte("Wrong params"))
+		logger.Log(0, "Missing params in HandleHeadlessSSOCallback")
+		return
+	}
+
+	// all responses should be in html format from here on out
+	w.Header().Add("content-type", "text/html; charset=utf-8")
+
+	// retrieve machinekey from state cache
+	reqKeyIf, machineKeyFoundErr := netcache.Get(state)
+	if machineKeyFoundErr != nil {
+		logger.Log(0, "requested machine state key expired before authorisation completed -", machineKeyFoundErr.Error())
+		response := returnErrTemplate("", "requested machine state key expired before authorisation completed", state, reqKeyIf)
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write(response)
+		return
+	}
+
+	_, err = logic.GetUser(userClaims.getUserName())
+	if err != nil { // user must not exists, so try to make one
+		if err = addUser(userClaims.getUserName()); err != nil {
+			logger.Log(1, "could not create new user: ", userClaims.getUserName())
+			return
+		}
+	}
+	newPass, fetchErr := fetchPassValue("")
+	if fetchErr != nil {
+		return
+	}
+	jwt, jwtErr := logic.VerifyAuthRequest(models.UserAuthParams{
+		UserName: userClaims.getUserName(),
+		Password: newPass,
+	})
+	if jwtErr != nil {
+		logger.Log(1, "could not parse jwt for user", userClaims.getUserName())
+		return
+	}
+
+	logger.Log(1, "headless SSO login by user:", userClaims.getUserName())
+
+	// Send OK to user in the browser
+	var response bytes.Buffer
+	if err := ssoCallbackTemplate.Execute(&response, ssoCallbackTemplateConfig{
+		User: userClaims.getUserName(),
+		Verb: "Authenticated",
+	}); err != nil {
+		logger.Log(0, "Could not render SSO callback template ", err.Error())
+		response := returnErrTemplate(userClaims.getUserName(), "Could not render SSO callback template", state, reqKeyIf)
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write(response)
+	} else {
+		w.WriteHeader(http.StatusOK)
+		w.Write(response.Bytes())
+	}
+	reqKeyIf.Pass = fmt.Sprintf("JWT: %s", jwt)
+	if err = netcache.Set(state, reqKeyIf); err != nil {
+		logger.Log(0, "failed to set netcache for user", reqKeyIf.User, "-", err.Error())
+	}
+}

+ 5 - 4
auth/nodecallback.go

@@ -13,7 +13,6 @@ import (
 	"github.com/gravitl/netmaker/logic/pro/netcache"
 	"github.com/gravitl/netmaker/models"
 	"github.com/gravitl/netmaker/models/promodels"
-	"github.com/gravitl/netmaker/servercfg"
 )
 
 var (
@@ -41,7 +40,7 @@ func HandleNodeSSOCallback(w http.ResponseWriter, r *http.Request) {
 	var userClaims, err = functions[get_user_info].(func(string, string) (*OAuthUser, error))(state, code)
 	if err != nil {
 		logger.Log(0, "error when getting user info from callback:", err.Error())
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		handleOauthNotConfigured(w)
 		return
 	}
 
@@ -135,7 +134,9 @@ func setNetcache(ncache *netcache.CValue, state string) error {
 
 func returnErrTemplate(uname, message, state string, ncache *netcache.CValue) []byte {
 	var response bytes.Buffer
-	ncache.Pass = message
+	if ncache != nil {
+		ncache.Pass = message
+	}
 	err := ssoErrCallbackTemplate.Execute(&response, ssoCallbackTemplateConfig{
 		User: uname,
 		Verb: message,
@@ -257,5 +258,5 @@ func isUserIsAllowed(username, network string, shouldAddUser bool) (*models.User
 		}
 	}
 
-	return &user, nil
+	return user, nil
 }

+ 1 - 2
auth/nodesession.go

@@ -1,7 +1,6 @@
 package auth
 
 import (
-	"encoding/hex"
 	"encoding/json"
 	"fmt"
 	"strings"
@@ -45,7 +44,7 @@ func SessionHandler(conn *websocket.Conn) {
 	req.Pass = ""
 	req.User = ""
 	// Add any extra parameter provided in the configuration to the Authorize Endpoint request??
-	stateStr := hex.EncodeToString([]byte(logic.RandomString(node_signin_length)))
+	stateStr := logic.RandomString(node_signin_length)
 	if err := netcache.Set(stateStr, req); err != nil {
 		logger.Log(0, "Failed to process sso request -", err.Error())
 		return

+ 4 - 7
auth/oidc.go

@@ -50,16 +50,13 @@ func initOIDC(redirectURL string, clientID string, clientSecret string, issuer s
 
 func handleOIDCLogin(w http.ResponseWriter, r *http.Request) {
 	var oauth_state_string = logic.RandomString(user_signin_length)
-	if auth_provider == nil && servercfg.GetFrontendURL() != "" {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
-		return
-	} else if auth_provider == nil {
-		fmt.Fprintf(w, "%s", []byte("no frontend URL was provided and an OAuth login was attempted\nplease reconfigure server to use OAuth or use basic credentials"))
+	if auth_provider == nil {
+		handleOauthNotConfigured(w)
 		return
 	}
 
 	if err := logic.SetState(oauth_state_string); err != nil {
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		handleOauthNotConfigured(w)
 		return
 	}
 	var url = auth_provider.AuthCodeURL(oauth_state_string)
@@ -73,7 +70,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
 	var content, err = getOIDCUserInfo(rState, rCode)
 	if err != nil {
 		logger.Log(1, "error when getting user info from callback:", err.Error())
-		http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?oauth=callback-error", http.StatusTemporaryRedirect)
+		handleOauthNotConfigured(w)
 		return
 	}
 	_, err = logic.GetUser(content.Email)

+ 46 - 0
cli/cmd/acl/allow.go

@@ -0,0 +1,46 @@
+package acl
+
+import (
+	"fmt"
+	"log"
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/logic/acls"
+	"github.com/spf13/cobra"
+)
+
+var aclAllowCmd = &cobra.Command{
+	Use:   "allow [NETWORK NAME] [FROM_NODE_NAME] [TO_NODE_NAME]",
+	Args:  cobra.ExactArgs(3),
+	Short: "Allow access from one node to another",
+	Long:  `Allow access from one node to another`,
+	Run: func(cmd *cobra.Command, args []string) {
+		nameIDMap := make(map[string]string)
+		for _, node := range *functions.GetNodes(args[0]) {
+			nameIDMap[strings.ToLower(node.Name)] = node.ID
+		}
+		fromNodeID, ok := nameIDMap[strings.ToLower(args[1])]
+		if !ok {
+			log.Fatalf("Node %s doesn't exist", args[1])
+		}
+		toNodeID, ok := nameIDMap[strings.ToLower(args[2])]
+		if !ok {
+			log.Fatalf("Node %s doesn't exist", args[2])
+		}
+		payload := acls.ACLContainer(map[acls.AclID]acls.ACL{
+			acls.AclID(fromNodeID): map[acls.AclID]byte{
+				acls.AclID(toNodeID): acls.Allowed,
+			},
+			acls.AclID(toNodeID): map[acls.AclID]byte{
+				acls.AclID(fromNodeID): acls.Allowed,
+			},
+		})
+		functions.UpdateACL(args[0], &payload)
+		fmt.Println("Success")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(aclAllowCmd)
+}

+ 46 - 0
cli/cmd/acl/deny.go

@@ -0,0 +1,46 @@
+package acl
+
+import (
+	"fmt"
+	"log"
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/logic/acls"
+	"github.com/spf13/cobra"
+)
+
+var aclDenyCmd = &cobra.Command{
+	Use:   "deny [NETWORK NAME] [FROM_NODE_NAME] [TO_NODE_NAME]",
+	Args:  cobra.ExactArgs(3),
+	Short: "Deny access from one node to another",
+	Long:  `Deny access from one node to another`,
+	Run: func(cmd *cobra.Command, args []string) {
+		nameIDMap := make(map[string]string)
+		for _, node := range *functions.GetNodes(args[0]) {
+			nameIDMap[strings.ToLower(node.Name)] = node.ID
+		}
+		fromNodeID, ok := nameIDMap[strings.ToLower(args[1])]
+		if !ok {
+			log.Fatalf("Node %s doesn't exist", args[1])
+		}
+		toNodeID, ok := nameIDMap[strings.ToLower(args[2])]
+		if !ok {
+			log.Fatalf("Node %s doesn't exist", args[2])
+		}
+		payload := acls.ACLContainer(map[acls.AclID]acls.ACL{
+			acls.AclID(fromNodeID): map[acls.AclID]byte{
+				acls.AclID(toNodeID): acls.NotAllowed,
+			},
+			acls.AclID(toNodeID): map[acls.AclID]byte{
+				acls.AclID(fromNodeID): acls.NotAllowed,
+			},
+		})
+		functions.UpdateACL(args[0], &payload)
+		fmt.Println("Success")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(aclDenyCmd)
+}

+ 46 - 0
cli/cmd/acl/list.go

@@ -0,0 +1,46 @@
+package acl
+
+import (
+	"os"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/logic/acls"
+	"github.com/guumaster/tablewriter"
+	"github.com/spf13/cobra"
+)
+
+var aclListCmd = &cobra.Command{
+	Use:   "list [NETWORK NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "List all ACLs associated with a network",
+	Long:  `List all ACLs associated with a network`,
+	Run: func(cmd *cobra.Command, args []string) {
+		aclSource := (map[acls.AclID]acls.ACL)(*functions.GetACL(args[0]))
+		nodes := functions.GetNodes(args[0])
+		idNameMap := make(map[string]string)
+		for _, node := range *nodes {
+			idNameMap[node.ID] = node.Name
+		}
+		table := tablewriter.NewWriter(os.Stdout)
+		table.SetHeader([]string{"From", "To", "Status"})
+		for id, acl := range aclSource {
+			for k, v := range (map[acls.AclID]byte)(acl) {
+				row := []string{idNameMap[string(id)], idNameMap[string(k)]}
+				switch v {
+				case acls.NotAllowed:
+					row = append(row, "Not Allowed")
+				case acls.NotPresent:
+					row = append(row, "Not Present")
+				case acls.Allowed:
+					row = append(row, "Allowed")
+				}
+				table.Append(row)
+			}
+		}
+		table.Render()
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(aclListCmd)
+}

+ 38 - 0
cli/cmd/acl/root.go

@@ -0,0 +1,38 @@
+package acl
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "acl",
+	Short: "Manage Access Control Lists (ACLs)",
+	Long:  `Manage Access Control Lists (ACLs)`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 20 - 0
cli/cmd/context/delete.go

@@ -0,0 +1,20 @@
+package context
+
+import (
+	"github.com/gravitl/netmaker/cli/config"
+	"github.com/spf13/cobra"
+)
+
+var contextDeleteCmd = &cobra.Command{
+	Use:   "delete [NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Delete a context",
+	Long:  `Delete a context`,
+	Run: func(cmd *cobra.Command, args []string) {
+		config.DeleteContext(args[0])
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(contextDeleteCmd)
+}

+ 20 - 0
cli/cmd/context/list.go

@@ -0,0 +1,20 @@
+package context
+
+import (
+	"github.com/gravitl/netmaker/cli/config"
+	"github.com/spf13/cobra"
+)
+
+var contextListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List all contexts",
+	Long:  `List all contexts`,
+	Run: func(cmd *cobra.Command, args []string) {
+		config.ListAll()
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(contextListCmd)
+}

+ 38 - 0
cli/cmd/context/root.go

@@ -0,0 +1,38 @@
+package context
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "context",
+	Short: "Manage various netmaker server configurations",
+	Long:  `Manage various netmaker server configurations`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 48 - 0
cli/cmd/context/set.go

@@ -0,0 +1,48 @@
+package context
+
+import (
+	"log"
+
+	"github.com/gravitl/netmaker/cli/config"
+	"github.com/spf13/cobra"
+)
+
+var (
+	endpoint  string
+	username  string
+	password  string
+	masterKey string
+	sso       bool
+)
+
+var contextSetCmd = &cobra.Command{
+	Use:   "set [NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Create a context or update an existing one",
+	Long:  `Create a context or update an existing one`,
+	Run: func(cmd *cobra.Command, args []string) {
+		ctx := config.Context{
+			Endpoint:  endpoint,
+			Username:  username,
+			Password:  password,
+			MasterKey: masterKey,
+			SSO:       sso,
+		}
+		if ctx.Username == "" && ctx.MasterKey == "" && !ctx.SSO {
+			cmd.Usage()
+			log.Fatal("Either username/password or master key is required")
+		}
+		config.SetContext(args[0], ctx)
+	},
+}
+
+func init() {
+	contextSetCmd.Flags().StringVar(&endpoint, "endpoint", "", "Endpoint of the API Server")
+	contextSetCmd.MarkFlagRequired("endpoint")
+	contextSetCmd.Flags().StringVar(&username, "username", "", "Username")
+	contextSetCmd.Flags().StringVar(&password, "password", "", "Password")
+	contextSetCmd.MarkFlagsRequiredTogether("username", "password")
+	contextSetCmd.Flags().BoolVar(&sso, "sso", false, "Login via Single Sign On (SSO) ?")
+	contextSetCmd.Flags().StringVar(&masterKey, "master_key", "", "Master Key")
+	rootCmd.AddCommand(contextSetCmd)
+}

+ 20 - 0
cli/cmd/context/use.go

@@ -0,0 +1,20 @@
+package context
+
+import (
+	"github.com/gravitl/netmaker/cli/config"
+	"github.com/spf13/cobra"
+)
+
+var contextUseCmd = &cobra.Command{
+	Use:   "use [NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Set the current context",
+	Long:  `Set the current context`,
+	Run: func(cmd *cobra.Command, args []string) {
+		config.SetCurrentContext(args[0])
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(contextUseCmd)
+}

+ 33 - 0
cli/cmd/dns/create.go

@@ -0,0 +1,33 @@
+package dns
+
+import (
+	"log"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var dnsCreateCmd = &cobra.Command{
+	Use:   "create",
+	Args:  cobra.NoArgs,
+	Short: "Create a DNS entry",
+	Long:  `Create a DNS entry`,
+	Run: func(cmd *cobra.Command, args []string) {
+		if address == "" && address6 == "" {
+			log.Fatal("Either IPv4 or IPv6 address is required")
+		}
+		dnsEntry := &models.DNSEntry{Name: dnsName, Address: address, Address6: address6, Network: networkName}
+		functions.PrettyPrint(functions.CreateDNS(networkName, dnsEntry))
+	},
+}
+
+func init() {
+	dnsCreateCmd.Flags().StringVar(&dnsName, "name", "", "Name of the DNS entry")
+	dnsCreateCmd.MarkFlagRequired("name")
+	dnsCreateCmd.Flags().StringVar(&networkName, "network", "", "Name of the Network")
+	dnsCreateCmd.MarkFlagRequired("network")
+	dnsCreateCmd.Flags().StringVar(&address, "ipv4_addr", "", "IPv4 Address")
+	dnsCreateCmd.Flags().StringVar(&address6, "ipv6_addr", "", "IPv6 Address")
+	rootCmd.AddCommand(dnsCreateCmd)
+}

+ 20 - 0
cli/cmd/dns/delete.go

@@ -0,0 +1,20 @@
+package dns
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var dnsDeleteCmd = &cobra.Command{
+	Use:   "delete [NETWORK NAME] [DOMAIN NAME]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete a DNS entry",
+	Long:  `Delete a DNS entry`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DeleteDNS(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(dnsDeleteCmd)
+}

+ 9 - 0
cli/cmd/dns/flags.go

@@ -0,0 +1,9 @@
+package dns
+
+var (
+	dnsName     string
+	address     string
+	address6    string
+	networkName string
+	dnsType     string
+)

+ 47 - 0
cli/cmd/dns/list.go

@@ -0,0 +1,47 @@
+package dns
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/guumaster/tablewriter"
+	"github.com/spf13/cobra"
+)
+
+var dnsListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List DNS entries",
+	Long:  `List DNS entries`,
+	Run: func(cmd *cobra.Command, args []string) {
+		var data []models.DNSEntry
+		if networkName != "" {
+			switch dnsType {
+			case "node":
+				data = *functions.GetNodeDNS(networkName)
+			case "custom":
+				data = *functions.GetCustomDNS(networkName)
+			case "network", "":
+				data = *functions.GetNetworkDNS(networkName)
+			default:
+				fmt.Println("Invalid DNS type provided ", dnsType)
+			}
+		} else {
+			data = *functions.GetDNS()
+		}
+		table := tablewriter.NewWriter(os.Stdout)
+		table.SetHeader([]string{"Name", "Network", "IPv4 Address", "IPv6 Address"})
+		for _, d := range data {
+			table.Append([]string{d.Name, d.Network, d.Address, d.Address6})
+		}
+		table.Render()
+	},
+}
+
+func init() {
+	dnsListCmd.Flags().StringVar(&networkName, "network", "", "Network name")
+	dnsListCmd.Flags().StringVar(&dnsType, "type", "", "Type of DNS records to fetch ENUM(node, custom, network)")
+	rootCmd.AddCommand(dnsListCmd)
+}

+ 22 - 0
cli/cmd/dns/push.go

@@ -0,0 +1,22 @@
+package dns
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var dnsPushCmd = &cobra.Command{
+	Use:   "push",
+	Args:  cobra.NoArgs,
+	Short: "Push latest DNS entries",
+	Long:  `Push latest DNS entries`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(*functions.PushDNS())
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(dnsPushCmd)
+}

+ 38 - 0
cli/cmd/dns/root.go

@@ -0,0 +1,38 @@
+package dns
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "dns",
+	Short: "Manage DNS entries associated with a network",
+	Long:  `Manage DNS entries associated with a network`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 22 - 0
cli/cmd/ext_client/config.go

@@ -0,0 +1,22 @@
+package ext_client
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var extClientConfigCmd = &cobra.Command{
+	Use:   "config [NETWORK NAME] [EXTERNAL CLIENT ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Get an External Client Configuration",
+	Long:  `Get an External Client Configuration`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(functions.GetExtClientConfig(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(extClientConfigCmd)
+}

+ 26 - 0
cli/cmd/ext_client/create.go

@@ -0,0 +1,26 @@
+package ext_client
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var extClientID string
+
+var extClientCreateCmd = &cobra.Command{
+	Use:   "create [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Create an External Client",
+	Long:  `Create an External Client`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.CreateExtClient(args[0], args[1], extClientID)
+		fmt.Println("Success")
+	},
+}
+
+func init() {
+	extClientCreateCmd.Flags().StringVar(&extClientID, "id", "", "ID of the external client")
+	rootCmd.AddCommand(extClientCreateCmd)
+}

+ 20 - 0
cli/cmd/ext_client/delete.go

@@ -0,0 +1,20 @@
+package ext_client
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var extClientDeleteCmd = &cobra.Command{
+	Use:   "delete [NETWORK NAME] [EXTERNAL CLIENT ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete an External Client",
+	Long:  `Delete an External Client`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DeleteExtClient(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(extClientDeleteCmd)
+}

+ 20 - 0
cli/cmd/ext_client/get.go

@@ -0,0 +1,20 @@
+package ext_client
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var extClientGetCmd = &cobra.Command{
+	Use:   "get [NETWORK NAME] [EXTERNAL CLIENT ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Get an External Client",
+	Long:  `Get an External Client`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetExtClient(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(extClientGetCmd)
+}

+ 40 - 0
cli/cmd/ext_client/list.go

@@ -0,0 +1,40 @@
+package ext_client
+
+import (
+	"os"
+	"strconv"
+	"time"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/guumaster/tablewriter"
+	"github.com/spf13/cobra"
+)
+
+var networkName string
+
+var extClientListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List External Clients",
+	Long:  `List External Clients`,
+	Run: func(cmd *cobra.Command, args []string) {
+		var data []models.ExtClient
+		if networkName != "" {
+			data = *functions.GetNetworkExtClients(networkName)
+		} else {
+			data = *functions.GetAllExtClients()
+		}
+		table := tablewriter.NewWriter(os.Stdout)
+		table.SetHeader([]string{"Client ID", "Network", "IPv4 Address", "IPv6 Address", "Enabled", "Last Modified"})
+		for _, d := range data {
+			table.Append([]string{d.ClientID, d.Network, d.Address, d.Address6, strconv.FormatBool(d.Enabled), time.Unix(d.LastModified, 0).String()})
+		}
+		table.Render()
+	},
+}
+
+func init() {
+	extClientListCmd.Flags().StringVar(&networkName, "network", "", "Network name")
+	rootCmd.AddCommand(extClientListCmd)
+}

+ 38 - 0
cli/cmd/ext_client/root.go

@@ -0,0 +1,38 @@
+package ext_client
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "ext_client",
+	Short: "Manage External Clients",
+	Long:  `Manage External Clients`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 71 - 0
cli/cmd/ext_client/update.go

@@ -0,0 +1,71 @@
+package ext_client
+
+import (
+	"encoding/json"
+	"log"
+	"os"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var (
+	extClientUpdateFile    string
+	description            string
+	privateKey             string
+	publicKey              string
+	address                string
+	address6               string
+	ingressGatewayID       string
+	ingressGatewayEndpoint string
+	ownerID                string
+)
+
+var extClientUpdateCmd = &cobra.Command{
+	Use:   "update [NETWORK NAME] [EXTERNAL CLIENT ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Update an External Client",
+	Long:  `Update an External Client`,
+	Run: func(cmd *cobra.Command, args []string) {
+		var (
+			network   = args[0]
+			clientID  = args[1]
+			extClient = &models.ExtClient{}
+		)
+		if extClientUpdateFile != "" {
+			content, err := os.ReadFile(extClientUpdateFile)
+			if err != nil {
+				log.Fatal("Error when opening file: ", err)
+			}
+			if err := json.Unmarshal(content, extClient); err != nil {
+				log.Fatal(err)
+			}
+		} else {
+			extClient.ClientID = clientID
+			extClient.Description = description
+			extClient.PrivateKey = privateKey
+			extClient.PublicKey = publicKey
+			extClient.Network = network
+			extClient.Address = address
+			extClient.Address6 = address6
+			extClient.IngressGatewayID = ingressGatewayID
+			extClient.IngressGatewayEndpoint = ingressGatewayEndpoint
+			extClient.OwnerID = ownerID
+		}
+		functions.PrettyPrint(functions.UpdateExtClient(network, clientID, extClient))
+	},
+}
+
+func init() {
+	extClientUpdateCmd.Flags().StringVar(&extClientUpdateFile, "file", "", "Filepath of updated external client definition in JSON")
+	extClientUpdateCmd.Flags().StringVar(&description, "desc", "", "Description of the external client")
+	extClientUpdateCmd.Flags().StringVar(&privateKey, "private_key", "", "Filepath of updated external client definition in JSON")
+	extClientUpdateCmd.Flags().StringVar(&publicKey, "public_key", "", "Filepath of updated external client definition in JSON")
+	extClientUpdateCmd.Flags().StringVar(&address, "ipv4_addr", "", "IPv4 address of the external client")
+	extClientUpdateCmd.Flags().StringVar(&address6, "ipv6_addr", "", "IPv6 address of the external client")
+	extClientUpdateCmd.Flags().StringVar(&ingressGatewayID, "ingress_gateway_id", "", "ID of the ingress gateway")
+	extClientUpdateCmd.Flags().StringVar(&ingressGatewayEndpoint, "ingress_gateway_endpoint", "", "Endpoint of the ingress gateway")
+	extClientUpdateCmd.Flags().StringVar(&ownerID, "owner_id", "", "External Client owner's ID")
+	rootCmd.AddCommand(extClientUpdateCmd)
+}

+ 35 - 0
cli/cmd/keys/create.go

@@ -0,0 +1,35 @@
+package keys
+
+import (
+	"log"
+	"strconv"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var keyName string
+
+var keysCreateCmd = &cobra.Command{
+	Use:   "create [NETWORK NAME] [NUM USES]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Create an access key",
+	Long:  `Create an access key`,
+	Run: func(cmd *cobra.Command, args []string) {
+		keyUses, err := strconv.ParseInt(args[1], 10, 64)
+		if err != nil {
+			log.Fatal(err)
+		}
+		key := &models.AccessKey{Uses: int(keyUses)}
+		if keyName != "" {
+			key.Name = keyName
+		}
+		functions.PrettyPrint(functions.CreateKey(args[0], key))
+	},
+}
+
+func init() {
+	keysCreateCmd.Flags().StringVar(&keyName, "name", "", "Name of the key")
+	rootCmd.AddCommand(keysCreateCmd)
+}

+ 23 - 0
cli/cmd/keys/delete.go

@@ -0,0 +1,23 @@
+package keys
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var keysDeleteCmd = &cobra.Command{
+	Use:   "delete [NETWORK NAME] [KEY NAME]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete a key",
+	Long:  `Delete a key`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.DeleteKey(args[0], args[1])
+		fmt.Println("Success")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(keysDeleteCmd)
+}

+ 20 - 0
cli/cmd/keys/list.go

@@ -0,0 +1,20 @@
+package keys
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var keysListCmd = &cobra.Command{
+	Use:   "list [NETWORK NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "List all keys associated with a network",
+	Long:  `List all keys associated with a network`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetKeys(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(keysListCmd)
+}

+ 38 - 0
cli/cmd/keys/root.go

@@ -0,0 +1,38 @@
+package keys
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "keys",
+	Short: "Manage access keys associated with a network",
+	Long:  `Manage access keys associated with a network`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 22 - 0
cli/cmd/logs.go

@@ -0,0 +1,22 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var getLogsCmd = &cobra.Command{
+	Use:   "logs",
+	Args:  cobra.NoArgs,
+	Short: "Retrieve server logs",
+	Long:  `Retrieve server logs`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(functions.GetLogs())
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(getLogsCmd)
+}

+ 20 - 0
cli/cmd/metrics/all.go

@@ -0,0 +1,20 @@
+package metrics
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var metricsAllCmd = &cobra.Command{
+	Use:   "all",
+	Args:  cobra.NoArgs,
+	Short: "Retrieve all metrics",
+	Long:  `Retrieve all metrics`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetAllMetrics())
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(metricsAllCmd)
+}

+ 20 - 0
cli/cmd/metrics/network.go

@@ -0,0 +1,20 @@
+package metrics
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var metricsNetworkCmd = &cobra.Command{
+	Use:   "network [NETWORK NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Retrieve network metrics",
+	Long:  `Retrieve network metrics`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetNetworkNodeMetrics(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(metricsNetworkCmd)
+}

+ 20 - 0
cli/cmd/metrics/network_ext.go

@@ -0,0 +1,20 @@
+package metrics
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var metricsNetworkExtCmd = &cobra.Command{
+	Use:   "network_ext [NETWORK NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Retrieve metrics of external clients on a given network",
+	Long:  `Retrieve metrics of external clients on a given network`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetNetworkExtMetrics(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(metricsNetworkExtCmd)
+}

+ 20 - 0
cli/cmd/metrics/node.go

@@ -0,0 +1,20 @@
+package metrics
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var metricsNodeCmd = &cobra.Command{
+	Use:   "node [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Retrieve node metrics",
+	Long:  `Retrieve node metrics`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetNodeMetrics(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(metricsNodeCmd)
+}

+ 38 - 0
cli/cmd/metrics/root.go

@@ -0,0 +1,38 @@
+package metrics
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "metrics",
+	Short: "Fetch metrics of nodes/networks",
+	Long:  `Fetch metrics of nodes/networks`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 85 - 0
cli/cmd/network/create.go

@@ -0,0 +1,85 @@
+package network
+
+import (
+	"encoding/json"
+	"log"
+	"os"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var networkCreateCmd = &cobra.Command{
+	Use:   "create",
+	Short: "Create a Network",
+	Long:  `Create a Network`,
+	Args:  cobra.NoArgs,
+	Run: func(cmd *cobra.Command, args []string) {
+		network := &models.Network{}
+		if networkDefinitionFilePath != "" {
+			content, err := os.ReadFile(networkDefinitionFilePath)
+			if err != nil {
+				log.Fatal("Error when opening file: ", err)
+			}
+			if err := json.Unmarshal(content, network); err != nil {
+				log.Fatal(err)
+			}
+		} else {
+			network.NetID = netID
+			network.AddressRange = address
+			if address6 != "" {
+				network.AddressRange6 = address6
+				network.IsIPv6 = "yes"
+			}
+			if udpHolePunch {
+				network.DefaultUDPHolePunch = "yes"
+			}
+			if localNetwork {
+				network.IsLocal = "yes"
+			}
+			if defaultACL {
+				network.DefaultACL = "yes"
+			}
+			if pointToSite {
+				network.IsPointToSite = "yes"
+			}
+			network.DefaultInterface = defaultInterface
+			network.DefaultListenPort = int32(defaultListenPort)
+			network.NodeLimit = int32(nodeLimit)
+			network.DefaultPostUp = defaultPostUp
+			network.DefaultPostDown = defaultPostDown
+			network.DefaultKeepalive = int32(defaultKeepalive)
+			if allowManualSignUp {
+				network.AllowManualSignUp = "yes"
+			}
+			network.LocalRange = localRange
+			network.DefaultExtClientDNS = defaultExtClientDNS
+			network.DefaultMTU = int32(defaultMTU)
+		}
+		functions.PrettyPrint(functions.CreateNetwork(network))
+	},
+}
+
+func init() {
+	networkCreateCmd.Flags().StringVar(&networkDefinitionFilePath, "file", "", "Path to network_definition.json")
+	networkCreateCmd.Flags().StringVar(&netID, "name", "", "Name of the network")
+	networkCreateCmd.MarkFlagsMutuallyExclusive("file", "name")
+	networkCreateCmd.Flags().StringVar(&address, "ipv4_addr", "", "IPv4 address of the network")
+	networkCreateCmd.Flags().StringVar(&address6, "ipv6_addr", "", "IPv6 address of the network")
+	networkCreateCmd.Flags().BoolVar(&udpHolePunch, "udp_hole_punch", false, "Enable UDP Hole Punching ?")
+	networkCreateCmd.Flags().BoolVar(&localNetwork, "local", false, "Is the network local (LAN) ?")
+	networkCreateCmd.Flags().BoolVar(&defaultACL, "default_acl", false, "Enable default Access Control List ?")
+	networkCreateCmd.Flags().BoolVar(&pointToSite, "point_to_site", false, "Enforce all clients to have only 1 central peer ?")
+	networkCreateCmd.Flags().StringVar(&defaultInterface, "interface", "", "Name of the network interface")
+	networkCreateCmd.Flags().StringVar(&defaultPostUp, "post_up", "", "Commands to run after server is up `;` separated")
+	networkCreateCmd.Flags().StringVar(&defaultPostDown, "post_down", "", "Commands to run after server is down `;` separated")
+	networkCreateCmd.Flags().StringVar(&localRange, "local_range", "", "Local CIDR range")
+	networkCreateCmd.Flags().StringVar(&defaultExtClientDNS, "ext_client_dns", "", "IPv4 address of DNS server to be used by external clients")
+	networkCreateCmd.Flags().IntVar(&defaultListenPort, "listen_port", 51821, "Default wireguard port each node will attempt to use")
+	networkCreateCmd.Flags().IntVar(&nodeLimit, "node_limit", 999999999, "Maximum number of nodes that can be associated with this network")
+	networkCreateCmd.Flags().IntVar(&defaultKeepalive, "keep_alive", 20, "Keep Alive in seconds")
+	networkCreateCmd.Flags().IntVar(&defaultMTU, "mtu", 1280, "MTU size")
+	networkCreateCmd.Flags().BoolVar(&allowManualSignUp, "manual_signup", false, "Allow manual signup ?")
+	rootCmd.AddCommand(networkCreateCmd)
+}

+ 22 - 0
cli/cmd/network/delete.go

@@ -0,0 +1,22 @@
+package network
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var networkDeleteCmd = &cobra.Command{
+	Use:   "delete [NAME]",
+	Short: "Delete a Network",
+	Long:  `Delete a Network`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(*functions.DeleteNetwork(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(networkDeleteCmd)
+}

+ 22 - 0
cli/cmd/network/flags.go

@@ -0,0 +1,22 @@
+package network
+
+var (
+	networkDefinitionFilePath string
+	netID                     string
+	address                   string
+	address6                  string
+	udpHolePunch              bool
+	localNetwork              bool
+	defaultACL                bool
+	pointToSite               bool
+	defaultInterface          string
+	defaultListenPort         int
+	nodeLimit                 int
+	defaultPostUp             string
+	defaultPostDown           string
+	defaultKeepalive          int
+	allowManualSignUp         bool
+	localRange                string
+	defaultExtClientDNS       string
+	defaultMTU                int
+)

+ 20 - 0
cli/cmd/network/get.go

@@ -0,0 +1,20 @@
+package network
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var networkGetCmd = &cobra.Command{
+	Use:   "get [NETWORK NAME]",
+	Short: "Get a Network",
+	Long:  `Get a Network`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetNetwork(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(networkGetCmd)
+}

+ 32 - 0
cli/cmd/network/list.go

@@ -0,0 +1,32 @@
+package network
+
+import (
+	"os"
+	"time"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/olekukonko/tablewriter"
+	"github.com/spf13/cobra"
+)
+
+var networkListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List all Networks",
+	Long:  `List all Networks`,
+	Args:  cobra.NoArgs,
+	Run: func(cmd *cobra.Command, args []string) {
+		networks := functions.GetNetworks()
+		table := tablewriter.NewWriter(os.Stdout)
+		table.SetHeader([]string{"NetId", "Address Range (IPv4)", "Address Range (IPv6)", "Network Last Modified", "Nodes Last Modified"})
+		for _, n := range *networks {
+			networkLastModified := time.Unix(n.NetworkLastModified, 0).Format(time.RFC3339)
+			nodesLastModified := time.Unix(n.NodesLastModified, 0).Format(time.RFC3339)
+			table.Append([]string{n.NetID, n.AddressRange, n.AddressRange6, networkLastModified, nodesLastModified})
+		}
+		table.Render()
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(networkListCmd)
+}

+ 27 - 0
cli/cmd/network/node_limit.go

@@ -0,0 +1,27 @@
+package network
+
+import (
+	"log"
+	"strconv"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var networkNodeLimitCmd = &cobra.Command{
+	Use:   "node_limit [NETWORK NAME] [NEW LIMIT]",
+	Short: "Update network nodel limit",
+	Long:  `Update network nodel limit`,
+	Args:  cobra.ExactArgs(2),
+	Run: func(cmd *cobra.Command, args []string) {
+		nodelimit, err := strconv.ParseInt(args[1], 10, 32)
+		if err != nil {
+			log.Fatal(err)
+		}
+		functions.PrettyPrint(functions.UpdateNetworkNodeLimit(args[0], int32(nodelimit)))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(networkNodeLimitCmd)
+}

+ 20 - 0
cli/cmd/network/refresh_keys.go

@@ -0,0 +1,20 @@
+package network
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var networkRefreshKeysCmd = &cobra.Command{
+	Use:   "refresh_keys [NETWORK NAME]",
+	Short: "Refresh public and private key pairs of a network",
+	Long:  `Refresh public and private key pairs of a network`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.RefreshKeys(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(networkRefreshKeysCmd)
+}

+ 40 - 0
cli/cmd/network/root.go

@@ -0,0 +1,40 @@
+package network
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "network",
+	Short: "Manage Netmaker Networks",
+	Long:  `Manage Netmaker Networks`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 86 - 0
cli/cmd/network/update.go

@@ -0,0 +1,86 @@
+package network
+
+import (
+	"encoding/json"
+	"log"
+	"os"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var networkUpdateCmd = &cobra.Command{
+	Use:   "update [NETWORK NAME]",
+	Short: "Update a Network",
+	Long:  `Update a Network`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		var (
+			networkName = args[0]
+			network     = &models.Network{}
+		)
+		if networkDefinitionFilePath != "" {
+			content, err := os.ReadFile(networkDefinitionFilePath)
+			if err != nil {
+				log.Fatal("Error when opening file: ", err)
+			}
+			if err := json.Unmarshal(content, network); err != nil {
+				log.Fatal(err)
+			}
+		} else {
+			network.NetID = networkName
+			network.AddressRange = address
+			if address6 != "" {
+				network.AddressRange6 = address6
+				network.IsIPv6 = "yes"
+			}
+			if udpHolePunch {
+				network.DefaultUDPHolePunch = "yes"
+			}
+			if localNetwork {
+				network.IsLocal = "yes"
+			}
+			if defaultACL {
+				network.DefaultACL = "yes"
+			}
+			if pointToSite {
+				network.IsPointToSite = "yes"
+			}
+			network.DefaultInterface = defaultInterface
+			network.DefaultListenPort = int32(defaultListenPort)
+			network.NodeLimit = int32(nodeLimit)
+			network.DefaultPostUp = defaultPostUp
+			network.DefaultPostDown = defaultPostDown
+			network.DefaultKeepalive = int32(defaultKeepalive)
+			if allowManualSignUp {
+				network.AllowManualSignUp = "yes"
+			}
+			network.LocalRange = localRange
+			network.DefaultExtClientDNS = defaultExtClientDNS
+			network.DefaultMTU = int32(defaultMTU)
+		}
+		functions.PrettyPrint(functions.UpdateNetwork(networkName, network))
+	},
+}
+
+func init() {
+	networkUpdateCmd.Flags().StringVar(&networkDefinitionFilePath, "file", "", "Path to network_definition.json")
+	networkUpdateCmd.Flags().StringVar(&address, "ipv4_addr", "", "IPv4 address of the network")
+	networkUpdateCmd.Flags().StringVar(&address6, "ipv6_addr", "", "IPv6 address of the network")
+	networkUpdateCmd.Flags().BoolVar(&udpHolePunch, "udp_hole_punch", false, "Enable UDP Hole Punching ?")
+	networkUpdateCmd.Flags().BoolVar(&localNetwork, "local", false, "Is the network local (LAN) ?")
+	networkUpdateCmd.Flags().BoolVar(&defaultACL, "default_acl", false, "Enable default Access Control List ?")
+	networkUpdateCmd.Flags().BoolVar(&pointToSite, "point_to_site", false, "Enforce all clients to have only 1 central peer ?")
+	networkUpdateCmd.Flags().StringVar(&defaultInterface, "interface", "", "Name of the network interface")
+	networkUpdateCmd.Flags().StringVar(&defaultPostUp, "post_up", "", "Commands to run after server is up `;` separated")
+	networkUpdateCmd.Flags().StringVar(&defaultPostDown, "post_down", "", "Commands to run after server is down `;` separated")
+	networkUpdateCmd.Flags().StringVar(&localRange, "local_range", "", "Local CIDR range")
+	networkUpdateCmd.Flags().StringVar(&defaultExtClientDNS, "ext_client_dns", "", "IPv4 address of DNS server to be used by external clients")
+	networkUpdateCmd.Flags().IntVar(&defaultListenPort, "listen_port", 0, "Default wireguard port each node will attempt to use")
+	networkUpdateCmd.Flags().IntVar(&nodeLimit, "node_limit", 0, "Maximum number of nodes that can be associated with this network")
+	networkUpdateCmd.Flags().IntVar(&defaultKeepalive, "keep_alive", 0, "Keep Alive in seconds")
+	networkUpdateCmd.Flags().IntVar(&defaultMTU, "mtu", 0, "MTU size")
+	networkUpdateCmd.Flags().BoolVar(&allowManualSignUp, "manual_signup", false, "Allow manual signup ?")
+	rootCmd.AddCommand(networkUpdateCmd)
+}

+ 43 - 0
cli/cmd/network_user/create.go

@@ -0,0 +1,43 @@
+package network_user
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models/promodels"
+	"github.com/spf13/cobra"
+)
+
+var networkuserCreateCmd = &cobra.Command{
+	Use:   "create [NETWORK NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Create a network user",
+	Long:  `Create a network user`,
+	Run: func(cmd *cobra.Command, args []string) {
+		user := &promodels.NetworkUser{
+			AccessLevel: accessLevel,
+			ClientLimit: clientLimit,
+			NodeLimit:   nodeLimit, ID: promodels.NetworkUserID(id),
+		}
+		if clients != "" {
+			user.Clients = strings.Split(clients, ",")
+		}
+		if nodes != "" {
+			user.Nodes = strings.Split(nodes, ",")
+		}
+		functions.CreateNetworkUser(args[0], user)
+		fmt.Println("Success")
+	},
+}
+
+func init() {
+	networkuserCreateCmd.Flags().IntVar(&accessLevel, "access_level", 0, "Custom access level")
+	networkuserCreateCmd.Flags().IntVar(&clientLimit, "client_limit", 0, "Maximum number of external clients that can be created")
+	networkuserCreateCmd.Flags().IntVar(&nodeLimit, "node_limit", 999999999, "Maximum number of nodes that can be attached to a network")
+	networkuserCreateCmd.Flags().StringVar(&clients, "clients", "", "Access to list of external clients (comma separated)")
+	networkuserCreateCmd.Flags().StringVar(&nodes, "nodes", "", "Access to list of nodes (comma separated)")
+	networkuserCreateCmd.Flags().StringVar(&id, "id", "", "ID of the network user")
+	networkuserCreateCmd.MarkFlagRequired("id")
+	rootCmd.AddCommand(networkuserCreateCmd)
+}

+ 23 - 0
cli/cmd/network_user/delete.go

@@ -0,0 +1,23 @@
+package network_user
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var networkuserDeleteCmd = &cobra.Command{
+	Use:   "delete [NETWORK NAME] [NETWORK USER NAME]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete a network user",
+	Long:  `Delete a network user`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.DeleteNetworkUser(args[0], args[1])
+		fmt.Println("Success")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(networkuserDeleteCmd)
+}

+ 10 - 0
cli/cmd/network_user/flags.go

@@ -0,0 +1,10 @@
+package network_user
+
+var (
+	accessLevel int
+	clientLimit int
+	nodeLimit   int
+	clients     string
+	nodes       string
+	id          string
+)

+ 27 - 0
cli/cmd/network_user/get.go

@@ -0,0 +1,27 @@
+package network_user
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var data bool
+
+var networkuserGetCmd = &cobra.Command{
+	Use:   "get [NETWORK NAME] [NETWORK USER NAME]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Fetch a network user",
+	Long:  `Fetch a network user`,
+	Run: func(cmd *cobra.Command, args []string) {
+		if data {
+			functions.PrettyPrint(functions.GetNetworkUserData(args[1]))
+		} else {
+			functions.PrettyPrint(functions.GetNetworkUser(args[0], args[1]))
+		}
+	},
+}
+
+func init() {
+	networkuserGetCmd.Flags().BoolVar(&data, "data", false, "Fetch entire data of a network user")
+	rootCmd.AddCommand(networkuserGetCmd)
+}

+ 27 - 0
cli/cmd/network_user/list.go

@@ -0,0 +1,27 @@
+package network_user
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var networkName string
+
+var networkuserListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List network users",
+	Long:  `List network users`,
+	Run: func(cmd *cobra.Command, args []string) {
+		if networkName != "" {
+			functions.PrettyPrint(functions.GetNetworkUsers(networkName))
+		} else {
+			functions.PrettyPrint(functions.GetAllNetworkUsers())
+		}
+	},
+}
+
+func init() {
+	networkuserListCmd.Flags().StringVar(&networkName, "network", "", "Name of the network")
+	rootCmd.AddCommand(networkuserListCmd)
+}

+ 38 - 0
cli/cmd/network_user/root.go

@@ -0,0 +1,38 @@
+package network_user
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "network_user",
+	Short: "Manage Network Users",
+	Long:  `Manage Network Users`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 43 - 0
cli/cmd/network_user/update.go

@@ -0,0 +1,43 @@
+package network_user
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models/promodels"
+	"github.com/spf13/cobra"
+)
+
+var networkuserUpdateCmd = &cobra.Command{
+	Use:   "update [NETWORK NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Update a network user",
+	Long:  `Update a network user`,
+	Run: func(cmd *cobra.Command, args []string) {
+		user := &promodels.NetworkUser{
+			AccessLevel: accessLevel,
+			ClientLimit: clientLimit,
+			NodeLimit:   nodeLimit, ID: promodels.NetworkUserID(id),
+		}
+		if clients != "" {
+			user.Clients = strings.Split(clients, ",")
+		}
+		if nodes != "" {
+			user.Nodes = strings.Split(nodes, ",")
+		}
+		functions.UpdateNetworkUser(args[0], user)
+		fmt.Println("Success")
+	},
+}
+
+func init() {
+	networkuserUpdateCmd.Flags().IntVar(&accessLevel, "access_level", 0, "Custom access level")
+	networkuserUpdateCmd.Flags().IntVar(&clientLimit, "client_limit", 0, "Maximum number of external clients that can be created")
+	networkuserUpdateCmd.Flags().IntVar(&nodeLimit, "node_limit", 999999999, "Maximum number of nodes that can be attached to a network")
+	networkuserUpdateCmd.Flags().StringVar(&clients, "clients", "", "Access to list of external clients (comma separated)")
+	networkuserUpdateCmd.Flags().StringVar(&nodes, "nodes", "", "Access to list of nodes (comma separated)")
+	networkuserUpdateCmd.Flags().StringVar(&id, "id", "", "ID of the network user")
+	networkuserUpdateCmd.MarkFlagRequired("id")
+	rootCmd.AddCommand(networkuserUpdateCmd)
+}

+ 34 - 0
cli/cmd/node/create_egress.go

@@ -0,0 +1,34 @@
+package node
+
+import (
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var nodeCreateEgressCmd = &cobra.Command{
+	Use:   "create_egress [NETWORK NAME] [NODE ID] [EGRESS GATEWAY ADDRESSES (comma separated)]",
+	Args:  cobra.ExactArgs(3),
+	Short: "Turn a Node into a Egress",
+	Long:  `Turn a Node into a Egress`,
+	Run: func(cmd *cobra.Command, args []string) {
+		egress := &models.EgressGatewayRequest{
+			NetID:     args[0],
+			NodeID:    args[1],
+			Interface: networkInterface,
+			Ranges:    strings.Split(args[2], ","),
+		}
+		if natEnabled {
+			egress.NatEnabled = "yes"
+		}
+		functions.PrettyPrint(functions.CreateEgress(args[0], args[1], egress))
+	},
+}
+
+func init() {
+	nodeCreateEgressCmd.Flags().StringVar(&networkInterface, "interface", "", "Network interface name (ex:- eth0)")
+	nodeCreateEgressCmd.Flags().BoolVar(&natEnabled, "nat", false, "Enable NAT for Egress Traffic ?")
+	rootCmd.AddCommand(nodeCreateEgressCmd)
+}

+ 21 - 0
cli/cmd/node/create_ingress.go

@@ -0,0 +1,21 @@
+package node
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var nodeCreateIngressCmd = &cobra.Command{
+	Use:   "create_ingress [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Turn a Node into a Ingress",
+	Long:  `Turn a Node into a Ingress`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.CreateIngress(args[0], args[1], failover))
+	},
+}
+
+func init() {
+	nodeCreateIngressCmd.Flags().BoolVar(&failover, "failover", false, "Enable FailOver ?")
+	rootCmd.AddCommand(nodeCreateIngressCmd)
+}

+ 22 - 0
cli/cmd/node/create_relay.go

@@ -0,0 +1,22 @@
+package node
+
+import (
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var nodeCreateRelayCmd = &cobra.Command{
+	Use:   "create_relay [NETWORK NAME] [NODE ID] [RELAY ADDRESSES (comma separated)]",
+	Args:  cobra.ExactArgs(3),
+	Short: "Turn a Node into a Relay",
+	Long:  `Turn a Node into a Relay`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.CreateRelay(args[0], args[1], strings.Split(args[2], ",")))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(nodeCreateRelayCmd)
+}

+ 20 - 0
cli/cmd/node/delete.go

@@ -0,0 +1,20 @@
+package node
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var nodeDeleteCmd = &cobra.Command{
+	Use:   "delete [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete a Node",
+	Long:  `Delete a Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DeleteNode(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(nodeDeleteCmd)
+}

+ 20 - 0
cli/cmd/node/delete_egress.go

@@ -0,0 +1,20 @@
+package node
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var nodeDeleteEgressCmd = &cobra.Command{
+	Use:   "delete_egress [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete Egress role from a Node",
+	Long:  `Delete Egress role from a Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DeleteEgress(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(nodeDeleteEgressCmd)
+}

+ 20 - 0
cli/cmd/node/delete_ingress.go

@@ -0,0 +1,20 @@
+package node
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var nodeDeleteIngressCmd = &cobra.Command{
+	Use:   "delete_ingress [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete Ingress role from a Node",
+	Long:  `Delete Ingress role from a Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DeleteIngress(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(nodeDeleteIngressCmd)
+}

+ 20 - 0
cli/cmd/node/delete_relay.go

@@ -0,0 +1,20 @@
+package node
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var nodeDeleteRelayCmd = &cobra.Command{
+	Use:   "delete_relay [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Delete Relay role from a Node",
+	Long:  `Delete Relay role from a Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.DeleteRelay(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(nodeDeleteRelayCmd)
+}

+ 28 - 0
cli/cmd/node/flags.go

@@ -0,0 +1,28 @@
+package node
+
+var (
+	networkInterface       string
+	natEnabled             bool
+	failover               bool
+	networkName            string
+	nodeDefinitionFilePath string
+	endpoint               string
+	listenPort             int
+	address                string
+	address6               string
+	localAddress           string
+	name                   string
+	postUp                 string
+	postDown               string
+	allowedIPs             string
+	keepAlive              int
+	relayAddrs             string
+	egressGatewayRanges    string
+	localRange             string
+	mtu                    int
+	expirationDateTime     int
+	defaultACL             bool
+	dnsOn                  bool
+	disconnect             bool
+	networkHub             bool
+)

+ 20 - 0
cli/cmd/node/get.go

@@ -0,0 +1,20 @@
+package node
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var nodeGetCmd = &cobra.Command{
+	Use:   "get [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Get a node by ID",
+	Long:  `Get a node by ID`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetNodeByID(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(nodeGetCmd)
+}

+ 47 - 0
cli/cmd/node/list.go

@@ -0,0 +1,47 @@
+package node
+
+import (
+	"os"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/guumaster/tablewriter"
+	"github.com/spf13/cobra"
+)
+
+// nodeListCmd lists all nodes
+var nodeListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List all nodes",
+	Long:  `List all nodes`,
+	Run: func(cmd *cobra.Command, args []string) {
+		var data []models.Node
+		if networkName != "" {
+			data = *functions.GetNodes(networkName)
+		} else {
+			data = *functions.GetNodes()
+		}
+		table := tablewriter.NewWriter(os.Stdout)
+		table.SetHeader([]string{"Name", "Addresses", "Version", "Network", "Egress", "Ingress", "Relay", "ID"})
+		for _, d := range data {
+			addresses := ""
+			if d.Address != "" {
+				addresses += d.Address
+			}
+			if d.Address6 != "" {
+				if d.Address != "" {
+					addresses += ", "
+				}
+				addresses += d.Address6
+			}
+			table.Append([]string{d.Name, addresses, d.Version, d.Network, d.IsEgressGateway, d.IsIngressGateway, d.IsRelay, d.ID})
+		}
+		table.Render()
+	},
+}
+
+func init() {
+	nodeListCmd.Flags().StringVar(&networkName, "network", "", "Network name specifier")
+	rootCmd.AddCommand(nodeListCmd)
+}

+ 38 - 0
cli/cmd/node/root.go

@@ -0,0 +1,38 @@
+package node
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "node",
+	Short: "Manage nodes associated with a network",
+	Long:  `Manage nodes associated with a network`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 22 - 0
cli/cmd/node/uncordon.go

@@ -0,0 +1,22 @@
+package node
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var nodeUncordonCmd = &cobra.Command{
+	Use:   "uncordon [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Get a node by ID",
+	Long:  `Get a node by ID`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(*functions.UncordonNode(args[0], args[1]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(nodeUncordonCmd)
+}

+ 100 - 0
cli/cmd/node/update.go

@@ -0,0 +1,100 @@
+package node
+
+import (
+	"encoding/json"
+	"log"
+	"os"
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var nodeUpdateCmd = &cobra.Command{
+	Use:   "update [NETWORK NAME] [NODE ID]",
+	Args:  cobra.ExactArgs(2),
+	Short: "Update a Node",
+	Long:  `Update a Node`,
+	Run: func(cmd *cobra.Command, args []string) {
+		var (
+			node        = &models.Node{}
+			networkName = args[0]
+			nodeID      = args[1]
+		)
+		if nodeDefinitionFilePath != "" {
+			content, err := os.ReadFile(nodeDefinitionFilePath)
+			if err != nil {
+				log.Fatal("Error when opening file: ", err)
+			}
+			if err := json.Unmarshal(content, node); err != nil {
+				log.Fatal(err)
+			}
+		} else {
+			if endpoint != "" {
+				node.Endpoint = endpoint
+				node.IsStatic = "no"
+			}
+			node.ListenPort = int32(listenPort)
+			node.Address = address
+			node.Address6 = address6
+			node.LocalAddress = localAddress
+			node.Name = name
+			node.PostUp = postUp
+			node.PostDown = postDown
+			if allowedIPs != "" {
+				node.AllowedIPs = strings.Split(allowedIPs, ",")
+			}
+			node.PersistentKeepalive = int32(keepAlive)
+			if relayAddrs != "" {
+				node.RelayAddrs = strings.Split(relayAddrs, ",")
+			}
+			if egressGatewayRanges != "" {
+				node.EgressGatewayRanges = strings.Split(egressGatewayRanges, ",")
+			}
+			if localRange != "" {
+				node.LocalRange = localRange
+				node.IsLocal = "yes"
+			}
+			node.MTU = int32(mtu)
+			node.ExpirationDateTime = int64(expirationDateTime)
+			if defaultACL {
+				node.DefaultACL = "yes"
+			}
+			if dnsOn {
+				node.DNSOn = "yes"
+			}
+			if disconnect {
+				node.Connected = "no"
+			}
+			if networkHub {
+				node.IsHub = "yes"
+			}
+		}
+		functions.PrettyPrint(functions.UpdateNode(networkName, nodeID, node))
+	},
+}
+
+func init() {
+	nodeUpdateCmd.Flags().StringVar(&nodeDefinitionFilePath, "file", "", "Filepath of updated node definition in JSON")
+	nodeUpdateCmd.Flags().StringVar(&endpoint, "endpoint", "", "Public endpoint of the node")
+	nodeUpdateCmd.Flags().IntVar(&listenPort, "listen_port", 0, "Default wireguard port for the node")
+	nodeUpdateCmd.Flags().StringVar(&address, "ipv4_addr", "", "IPv4 address of the node")
+	nodeUpdateCmd.Flags().StringVar(&address6, "ipv6_addr", "", "IPv6 address of the node")
+	nodeUpdateCmd.Flags().StringVar(&localAddress, "local_addr", "", "Locally reachable address of the node")
+	nodeUpdateCmd.Flags().StringVar(&name, "name", "", "Node name")
+	nodeUpdateCmd.Flags().StringVar(&postUp, "post_up", "", "Commands to run after node is up `;` separated")
+	nodeUpdateCmd.Flags().StringVar(&postDown, "post_down", "", "Commands to run after node is down `;` separated")
+	nodeUpdateCmd.Flags().StringVar(&allowedIPs, "allowed_addrs", "", "Additional private addresses given to the node (comma separated)")
+	nodeUpdateCmd.Flags().IntVar(&keepAlive, "keep_alive", 0, "Interval in which packets are sent to keep connections open with peers")
+	nodeUpdateCmd.Flags().StringVar(&relayAddrs, "relay_addrs", "", "Addresses for relaying connections if node acts as a relay")
+	nodeUpdateCmd.Flags().StringVar(&egressGatewayRanges, "egress_addrs", "", "Addresses for egressing traffic if node acts as an egress")
+	nodeUpdateCmd.Flags().StringVar(&localRange, "local_range", "", "Local range in which the node will look for private addresses to use as an endpoint if `LocalNetwork` is enabled")
+	nodeUpdateCmd.Flags().IntVar(&mtu, "mtu", 0, "MTU size")
+	nodeUpdateCmd.Flags().IntVar(&expirationDateTime, "expiry", 0, "UNIX timestamp after which node will lose access to the network")
+	nodeUpdateCmd.Flags().BoolVar(&defaultACL, "acl", false, "Enable default ACL ?")
+	nodeUpdateCmd.Flags().BoolVar(&dnsOn, "dns", false, "Setup DNS entries for peers locally ?")
+	nodeUpdateCmd.Flags().BoolVar(&disconnect, "disconnect", false, "Disconnect from the network ?")
+	nodeUpdateCmd.Flags().BoolVar(&networkHub, "hub", false, "On a point to site network, this node is the only one which all peers connect to ?")
+	rootCmd.AddCommand(nodeUpdateCmd)
+}

+ 69 - 0
cli/cmd/root.go

@@ -0,0 +1,69 @@
+package cmd
+
+import (
+	"os"
+
+	"github.com/gravitl/netmaker/cli/cmd/acl"
+	"github.com/gravitl/netmaker/cli/cmd/context"
+	"github.com/gravitl/netmaker/cli/cmd/dns"
+	"github.com/gravitl/netmaker/cli/cmd/ext_client"
+	"github.com/gravitl/netmaker/cli/cmd/keys"
+	"github.com/gravitl/netmaker/cli/cmd/metrics"
+	"github.com/gravitl/netmaker/cli/cmd/network"
+	"github.com/gravitl/netmaker/cli/cmd/network_user"
+	"github.com/gravitl/netmaker/cli/cmd/node"
+	"github.com/gravitl/netmaker/cli/cmd/server"
+	"github.com/gravitl/netmaker/cli/cmd/user"
+	"github.com/gravitl/netmaker/cli/cmd/usergroup"
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "nmctl",
+	Short: "CLI for interacting with Netmaker Server",
+	Long:  `CLI for interacting with Netmaker Server`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root of all subcommands
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+
+	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.tctl.yaml)")
+
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+
+	// IMP: Bind subcommands here
+	rootCmd.AddCommand(network.GetRoot())
+	rootCmd.AddCommand(context.GetRoot())
+	rootCmd.AddCommand(keys.GetRoot())
+	rootCmd.AddCommand(acl.GetRoot())
+	rootCmd.AddCommand(node.GetRoot())
+	rootCmd.AddCommand(dns.GetRoot())
+	rootCmd.AddCommand(server.GetRoot())
+	rootCmd.AddCommand(ext_client.GetRoot())
+	rootCmd.AddCommand(user.GetRoot())
+	rootCmd.AddCommand(usergroup.GetRoot())
+	rootCmd.AddCommand(metrics.GetRoot())
+	rootCmd.AddCommand(network_user.GetRoot())
+}

+ 20 - 0
cli/cmd/server/config.go

@@ -0,0 +1,20 @@
+package server
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var serverConfigCmd = &cobra.Command{
+	Use:   "config",
+	Args:  cobra.NoArgs,
+	Short: "Retrieve server config",
+	Long:  `Retrieve server config`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetServerConfig())
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(serverConfigCmd)
+}

+ 22 - 0
cli/cmd/server/has_admin.go

@@ -0,0 +1,22 @@
+package server
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var serverHasAdminCmd = &cobra.Command{
+	Use:   "has_admin",
+	Args:  cobra.NoArgs,
+	Short: "Check if server has an admin",
+	Long:  `Check if server has an admin`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(*functions.HasAdmin())
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(serverHasAdminCmd)
+}

+ 20 - 0
cli/cmd/server/health.go

@@ -0,0 +1,20 @@
+package server
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var serverHealthCmd = &cobra.Command{
+	Use:   "health",
+	Args:  cobra.NoArgs,
+	Short: "View server health",
+	Long:  `View server health`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetServerHealth())
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(serverHealthCmd)
+}

+ 20 - 0
cli/cmd/server/info.go

@@ -0,0 +1,20 @@
+package server
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var serverInfoCmd = &cobra.Command{
+	Use:   "info",
+	Args:  cobra.NoArgs,
+	Short: "Retrieve server information",
+	Long:  `Retrieve server information`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetServerInfo())
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(serverInfoCmd)
+}

+ 38 - 0
cli/cmd/server/root.go

@@ -0,0 +1,38 @@
+package server
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "server",
+	Short: "Get netmaker server information",
+	Long:  `Get netmaker server information`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 37 - 0
cli/cmd/user/create.go

@@ -0,0 +1,37 @@
+package user
+
+import (
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var userCreateCmd = &cobra.Command{
+	Use:   "create",
+	Args:  cobra.NoArgs,
+	Short: "Create a new user",
+	Long:  `Create a new user`,
+	Run: func(cmd *cobra.Command, args []string) {
+		user := &models.User{UserName: username, Password: password, IsAdmin: admin}
+		if networks != "" {
+			user.Networks = strings.Split(networks, ",")
+		}
+		if groups != "" {
+			user.Groups = strings.Split(groups, ",")
+		}
+		functions.PrettyPrint(functions.CreateUser(user))
+	},
+}
+
+func init() {
+	userCreateCmd.Flags().StringVar(&username, "name", "", "Name of the user")
+	userCreateCmd.Flags().StringVar(&password, "password", "", "Password of the user")
+	userCreateCmd.MarkFlagRequired("name")
+	userCreateCmd.MarkFlagRequired("password")
+	userCreateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ?")
+	userCreateCmd.Flags().StringVar(&networks, "networks", "", "List of networks the user will access to (comma separated)")
+	userCreateCmd.Flags().StringVar(&groups, "groups", "", "List of user groups the user will be part of (comma separated)")
+	rootCmd.AddCommand(userCreateCmd)
+}

+ 20 - 0
cli/cmd/user/delete.go

@@ -0,0 +1,20 @@
+package user
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var userDeleteCmd = &cobra.Command{
+	Use:   "delete [USER NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Delete a user",
+	Long:  `Delete a user`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(*functions.DeleteUser(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(userDeleteCmd)
+}

+ 9 - 0
cli/cmd/user/flags.go

@@ -0,0 +1,9 @@
+package user
+
+var (
+	username string
+	password string
+	admin    bool
+	networks string
+	groups   string
+)

+ 20 - 0
cli/cmd/user/get.go

@@ -0,0 +1,20 @@
+package user
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var userGetCmd = &cobra.Command{
+	Use:   "get [USER NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Get a user",
+	Long:  `Get a user`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetUser(args[0]))
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(userGetCmd)
+}

+ 30 - 0
cli/cmd/user/list.go

@@ -0,0 +1,30 @@
+package user
+
+import (
+	"os"
+	"strconv"
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/guumaster/tablewriter"
+	"github.com/spf13/cobra"
+)
+
+var userListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List all users",
+	Long:  `List all users`,
+	Run: func(cmd *cobra.Command, args []string) {
+		table := tablewriter.NewWriter(os.Stdout)
+		table.SetHeader([]string{"Name", "Admin", "Networks", "Groups"})
+		for _, d := range *functions.ListUsers() {
+			table.Append([]string{d.UserName, strconv.FormatBool(d.IsAdmin), strings.Join(d.Networks, ", "), strings.Join(d.Groups, ", ")})
+		}
+		table.Render()
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(userListCmd)
+}

+ 38 - 0
cli/cmd/user/root.go

@@ -0,0 +1,38 @@
+package user
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "user",
+	Short: "Manage users and permissions",
+	Long:  `Manage users and permissions`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 35 - 0
cli/cmd/user/update.go

@@ -0,0 +1,35 @@
+package user
+
+import (
+	"strings"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+var userUpdateCmd = &cobra.Command{
+	Use:   "update [USER NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Update a user",
+	Long:  `Update a user`,
+	Run: func(cmd *cobra.Command, args []string) {
+		user := &models.User{UserName: args[0], IsAdmin: admin}
+		if networks != "" {
+			user.Networks = strings.Split(networks, ",")
+		}
+		if groups != "" {
+			user.Groups = strings.Split(groups, ",")
+		} else {
+			user.Groups = []string{"*"}
+		}
+		functions.PrettyPrint(functions.UpdateUser(user))
+	},
+}
+
+func init() {
+	userUpdateCmd.Flags().BoolVar(&admin, "admin", false, "Make the user an admin ?")
+	userUpdateCmd.Flags().StringVar(&networks, "networks", "", "List of networks the user will access to (comma separated)")
+	userUpdateCmd.Flags().StringVar(&groups, "groups", "", "List of user groups the user will be part of (comma separated)")
+	rootCmd.AddCommand(userUpdateCmd)
+}

+ 23 - 0
cli/cmd/usergroup/create.go

@@ -0,0 +1,23 @@
+package usergroup
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var usergroupCreateCmd = &cobra.Command{
+	Use:   "create [GROUP NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Create a usergroup",
+	Long:  `Create a usergroup`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.CreateUsergroup(args[0])
+		fmt.Println("Success")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(usergroupCreateCmd)
+}

+ 23 - 0
cli/cmd/usergroup/delete.go

@@ -0,0 +1,23 @@
+package usergroup
+
+import (
+	"fmt"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var usergroupDeleteCmd = &cobra.Command{
+	Use:   "delete [GROUP NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Delete a usergroup",
+	Long:  `Delete a usergroup`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.DeleteUsergroup(args[0])
+		fmt.Println("Success")
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(usergroupDeleteCmd)
+}

+ 20 - 0
cli/cmd/usergroup/get.go

@@ -0,0 +1,20 @@
+package usergroup
+
+import (
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/spf13/cobra"
+)
+
+var usergroupGetCmd = &cobra.Command{
+	Use:   "get",
+	Args:  cobra.NoArgs,
+	Short: "Fetch all usergroups",
+	Long:  `Fetch all usergroups`,
+	Run: func(cmd *cobra.Command, args []string) {
+		functions.PrettyPrint(functions.GetUsergroups())
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(usergroupGetCmd)
+}

+ 38 - 0
cli/cmd/usergroup/root.go

@@ -0,0 +1,38 @@
+package usergroup
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "usergroup",
+	Short: "Manage User Groups",
+	Long:  `Manage User Groups`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+// GetRoot returns the root subcommand
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 145 - 0
cli/config/config.go

@@ -0,0 +1,145 @@
+package config
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+
+	"gopkg.in/yaml.v3"
+)
+
+// Context maintains configuration for interaction with Netmaker API
+type Context struct {
+	Endpoint  string `yaml:"endpoint"`
+	Username  string `yaml:"username,omitempty"`
+	Password  string `yaml:"password,omitempty"`
+	MasterKey string `yaml:"masterkey,omitempty"`
+	Current   bool   `yaml:"current,omitempty"`
+	AuthToken string `yaml:"auth_token,omitempty"`
+	SSO       bool   `yaml:"sso,omitempty"`
+}
+
+var (
+	contextMap     = map[string]Context{}
+	configFilePath string
+	filename       string
+)
+
+func createConfigPathIfNotExists() {
+	homeDir, err := os.UserHomeDir()
+	if err != nil {
+		log.Fatal(err)
+	}
+	configFilePath = filepath.Join(homeDir, ".netmaker")
+	// create directory if not exists
+	if err := os.MkdirAll(configFilePath, os.ModePerm); err != nil {
+		log.Fatal(err)
+	}
+	filename = filepath.Join(configFilePath, "config.yml")
+	// create file if not exists
+	if _, err := os.Stat(filename); err != nil {
+		if os.IsNotExist(err) {
+			if _, err := os.Create(filename); err != nil {
+				log.Fatalf("Unable to create file filename: %s", err)
+			}
+		} else {
+			log.Fatal(err)
+		}
+	}
+}
+
+func loadConfig() {
+	content, err := os.ReadFile(filename)
+	if err != nil {
+		log.Fatalf("Error reading config file: %s", err)
+	}
+	if err := yaml.Unmarshal(content, &contextMap); err != nil {
+		log.Fatalf("Unable to decode YAML into struct: %s", err)
+	}
+}
+
+func saveContext() {
+	bodyBytes, err := yaml.Marshal(&contextMap)
+	if err != nil {
+		log.Fatalf("Error marshalling into YAML %s", err)
+	}
+	file, err := os.Create(filename)
+	if err != nil {
+		log.Fatal(err)
+	}
+	if _, err := file.Write(bodyBytes); err != nil {
+		log.Fatal(err)
+	}
+	if err := file.Close(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+// GetCurrentContext - returns current set context
+func GetCurrentContext() (name string, ctx Context) {
+	for n, c := range contextMap {
+		if c.Current {
+			name, ctx = n, c
+			return
+		}
+	}
+	log.Fatalf("No current context set, do so via `netmaker context use <name>`")
+	return
+}
+
+// SetCurrentContext - sets a given context as current context
+func SetCurrentContext(ctxName string) {
+	if _, ok := contextMap[ctxName]; !ok {
+		log.Fatalf("No such context %s", ctxName)
+	}
+	for key, ctx := range contextMap {
+		ctx.Current = key == ctxName
+		contextMap[key] = ctx
+	}
+	saveContext()
+}
+
+// SetContext - updates an existing context or creates a new one
+func SetContext(ctxName string, ctx Context) {
+	if oldCtx, ok := contextMap[ctxName]; ok && oldCtx.Current {
+		ctx.Current = true
+	}
+	contextMap[ctxName] = ctx
+	saveContext()
+}
+
+// SetAuthToken - saves the auth token
+func SetAuthToken(authToken string) {
+	ctxName, _ := GetCurrentContext()
+	if ctx, ok := contextMap[ctxName]; ok {
+		ctx.AuthToken = authToken
+		contextMap[ctxName] = ctx
+		saveContext()
+	}
+}
+
+// DeleteContext - deletes a context
+func DeleteContext(ctxName string) {
+	if _, ok := contextMap[ctxName]; ok {
+		delete(contextMap, ctxName)
+		saveContext()
+	} else {
+		log.Fatalf("No such context %s", ctxName)
+	}
+}
+
+// ListAll - lists all contexts
+func ListAll() {
+	for key, ctx := range contextMap {
+		fmt.Print("\n", key, " -> ", ctx.Endpoint)
+		if ctx.Current {
+			fmt.Print(" (current)")
+		}
+	}
+}
+
+func init() {
+	createConfigPathIfNotExists()
+	loadConfig()
+}

+ 18 - 0
cli/functions/acl.go

@@ -0,0 +1,18 @@
+package functions
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/logic/acls"
+)
+
+// GetACL - fetch all ACLs associated with a network
+func GetACL(networkName string) *acls.ACLContainer {
+	return request[acls.ACLContainer](http.MethodGet, fmt.Sprintf("/api/networks/%s/acls", networkName), nil)
+}
+
+// UpdateACL - update an ACL
+func UpdateACL(networkName string, payload *acls.ACLContainer) *acls.ACLContainer {
+	return request[acls.ACLContainer](http.MethodPut, fmt.Sprintf("/api/networks/%s/acls", networkName), payload)
+}

+ 43 - 0
cli/functions/dns.go

@@ -0,0 +1,43 @@
+package functions
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+// GetDNS - fetch all DNS entries
+func GetDNS() *[]models.DNSEntry {
+	return request[[]models.DNSEntry](http.MethodGet, "/api/dns", nil)
+}
+
+// GetNodeDNS - fetch all Node DNS entires
+func GetNodeDNS(networkName string) *[]models.DNSEntry {
+	return request[[]models.DNSEntry](http.MethodGet, fmt.Sprintf("/api/dns/adm/%s/nodes", networkName), nil)
+}
+
+// GetCustomDNS - fetch user defined DNS entriees
+func GetCustomDNS(networkName string) *[]models.DNSEntry {
+	return request[[]models.DNSEntry](http.MethodGet, fmt.Sprintf("/api/dns/adm/%s/custom", networkName), nil)
+}
+
+// GetNetworkDNS - fetch DNS entries associated with a network
+func GetNetworkDNS(networkName string) *[]models.DNSEntry {
+	return request[[]models.DNSEntry](http.MethodGet, "/api/dns/adm/"+networkName, nil)
+}
+
+// CreateDNS - create a DNS entry
+func CreateDNS(networkName string, payload *models.DNSEntry) *models.DNSEntry {
+	return request[models.DNSEntry](http.MethodPost, "/api/dns/"+networkName, payload)
+}
+
+// PushDNS - push a DNS entry to CoreDNS
+func PushDNS() *string {
+	return request[string](http.MethodPost, "/api/dns/adm/pushdns", nil)
+}
+
+// DeleteDNS - delete a DNS entry
+func DeleteDNS(networkName, domainName string) *string {
+	return request[string](http.MethodDelete, fmt.Sprintf("/api/dns/%s/%s", networkName, domainName), nil)
+}

+ 49 - 0
cli/functions/ext_client.go

@@ -0,0 +1,49 @@
+package functions
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+// GetAllExtClients - fetch all external clients
+func GetAllExtClients() *[]models.ExtClient {
+	return request[[]models.ExtClient](http.MethodGet, "/api/extclients", nil)
+}
+
+// GetNetworkExtClients - fetch external clients associated with a network
+func GetNetworkExtClients(networkName string) *[]models.ExtClient {
+	return request[[]models.ExtClient](http.MethodGet, "/api/extclients/"+networkName, nil)
+}
+
+// GetExtClient - fetch a single external client
+func GetExtClient(networkName, clientID string) *models.ExtClient {
+	return request[models.ExtClient](http.MethodGet, fmt.Sprintf("/api/extclients/%s/%s", networkName, clientID), nil)
+}
+
+// GetExtClientConfig - fetch a wireguard config of an external client
+func GetExtClientConfig(networkName, clientID string) string {
+	return get(fmt.Sprintf("/api/extclients/%s/%s/file", networkName, clientID))
+}
+
+// CreateExtClient - create an external client
+func CreateExtClient(networkName, nodeID, extClientID string) {
+	if extClientID != "" {
+		request[any](http.MethodPost, fmt.Sprintf("/api/extclients/%s/%s", networkName, nodeID), &models.CustomExtClient{
+			ClientID: extClientID,
+		})
+	} else {
+		request[any](http.MethodPost, fmt.Sprintf("/api/extclients/%s/%s", networkName, nodeID), nil)
+	}
+}
+
+// DeleteExtClient - delete an external client
+func DeleteExtClient(networkName, clientID string) *models.SuccessResponse {
+	return request[models.SuccessResponse](http.MethodDelete, fmt.Sprintf("/api/extclients/%s/%s", networkName, clientID), nil)
+}
+
+// UpdateExtClient - update an external client
+func UpdateExtClient(networkName, clientID string, payload *models.ExtClient) *models.ExtClient {
+	return request[models.ExtClient](http.MethodPut, fmt.Sprintf("/api/extclients/%s/%s", networkName, clientID), payload)
+}

+ 189 - 0
cli/functions/http_client.go

@@ -0,0 +1,189 @@
+package functions
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"os/signal"
+	"strings"
+
+	"github.com/gorilla/websocket"
+	"github.com/gravitl/netmaker/cli/config"
+	"github.com/gravitl/netmaker/logger"
+	"github.com/gravitl/netmaker/models"
+)
+
+func ssoLogin(endpoint string) string {
+	var (
+		authToken string
+		interrupt = make(chan os.Signal, 1)
+		url, _    = url.Parse(endpoint)
+		socketURL = fmt.Sprintf("wss://%s/api/oauth/headless", url.Host)
+	)
+	signal.Notify(interrupt, os.Interrupt)
+	conn, _, err := websocket.DefaultDialer.Dial(socketURL, nil)
+	if err != nil {
+		log.Fatal("error connecting to endpoint ", socketURL, err.Error())
+	}
+	defer conn.Close()
+	_, msg, err := conn.ReadMessage()
+	if err != nil {
+		log.Fatal("error reading from server: ", err.Error())
+	}
+	fmt.Printf("Please visit:\n %s \n to authenticate\n", string(msg))
+	done := make(chan struct{})
+	defer close(done)
+	go func() {
+		for {
+			msgType, msg, err := conn.ReadMessage()
+			if err != nil {
+				if msgType < 0 {
+					done <- struct{}{}
+					return
+				}
+				if !strings.Contains(err.Error(), "normal") {
+					log.Fatal("read error: ", err.Error())
+				}
+				return
+			}
+			if msgType == websocket.CloseMessage {
+				done <- struct{}{}
+				return
+			}
+			if strings.Contains(string(msg), "JWT: ") {
+				authToken = strings.TrimPrefix(string(msg), "JWT: ")
+			} else {
+				logger.Log(0, "Message from server:", string(msg))
+				return
+			}
+		}
+	}()
+	for {
+		select {
+		case <-done:
+			return authToken
+		case <-interrupt:
+			err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+			if err != nil {
+				logger.Log(0, "write close:", err.Error())
+			}
+			return authToken
+		}
+	}
+}
+
+func getAuthToken(ctx config.Context, force bool) string {
+	if !force && ctx.AuthToken != "" {
+		return ctx.AuthToken
+	}
+	if ctx.SSO {
+		authToken := ssoLogin(ctx.Endpoint)
+		config.SetAuthToken(authToken)
+		return authToken
+	}
+	authParams := &models.UserAuthParams{UserName: ctx.Username, Password: ctx.Password}
+	payload, err := json.Marshal(authParams)
+	if err != nil {
+		log.Fatal(err)
+	}
+	res, err := http.Post(ctx.Endpoint+"/api/users/adm/authenticate", "application/json", bytes.NewReader(payload))
+	if err != nil {
+		log.Fatal(err)
+	}
+	resBodyBytes, err := io.ReadAll(res.Body)
+	if err != nil {
+		log.Fatalf("Client could not read response body: %s", err)
+	}
+	if res.StatusCode != http.StatusOK {
+		log.Fatalf("Error Status: %d Response: %s", res.StatusCode, string(resBodyBytes))
+	}
+	body := new(models.SuccessResponse)
+	if err := json.Unmarshal(resBodyBytes, body); err != nil {
+		log.Fatalf("Error unmarshalling JSON: %s", err)
+	}
+	authToken := body.Response.(map[string]any)["AuthToken"].(string)
+	config.SetAuthToken(authToken)
+	return authToken
+}
+
+func request[T any](method, route string, payload any) *T {
+	var (
+		_, ctx = config.GetCurrentContext()
+		req    *http.Request
+		err    error
+	)
+	if payload == nil {
+		req, err = http.NewRequest(method, ctx.Endpoint+route, nil)
+		if err != nil {
+			log.Fatalf("Client could not create request: %s", err)
+		}
+	} else {
+		payloadBytes, jsonErr := json.Marshal(payload)
+		if jsonErr != nil {
+			log.Fatalf("Error in request JSON marshalling: %s", err)
+		}
+		req, err = http.NewRequest(method, ctx.Endpoint+route, bytes.NewReader(payloadBytes))
+		if err != nil {
+			log.Fatalf("Client could not create request: %s", err)
+		}
+		req.Header.Set("Content-Type", "application/json")
+	}
+	if ctx.MasterKey != "" {
+		req.Header.Set("Authorization", "Bearer "+ctx.MasterKey)
+	} else {
+		req.Header.Set("Authorization", "Bearer "+getAuthToken(ctx, false))
+	}
+	retried := false
+retry:
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		log.Fatalf("Client error making http request: %s", err)
+	}
+	// refresh JWT token
+	if res.StatusCode == http.StatusUnauthorized && !retried && ctx.MasterKey == "" {
+		req.Header.Set("Authorization", "Bearer "+getAuthToken(ctx, true))
+		retried = true
+		goto retry
+	}
+	resBodyBytes, err := io.ReadAll(res.Body)
+	if err != nil {
+		log.Fatalf("Client could not read response body: %s", err)
+	}
+	if res.StatusCode != http.StatusOK {
+		log.Fatalf("Error Status: %d Response: %s", res.StatusCode, string(resBodyBytes))
+	}
+	body := new(T)
+	if len(resBodyBytes) > 0 {
+		if err := json.Unmarshal(resBodyBytes, body); err != nil {
+			log.Fatalf("Error unmarshalling JSON: %s", err)
+		}
+	}
+	return body
+}
+
+func get(route string) string {
+	_, ctx := config.GetCurrentContext()
+	req, err := http.NewRequest(http.MethodGet, ctx.Endpoint+route, nil)
+	if err != nil {
+		log.Fatal(err)
+	}
+	if ctx.MasterKey != "" {
+		req.Header.Set("Authorization", "Bearer "+ctx.MasterKey)
+	} else {
+		req.Header.Set("Authorization", "Bearer "+getAuthToken(ctx, true))
+	}
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		log.Fatal(err)
+	}
+	bodyBytes, err := io.ReadAll(res.Body)
+	if err != nil {
+		log.Fatal(err)
+	}
+	return string(bodyBytes)
+}

+ 23 - 0
cli/functions/keys.go

@@ -0,0 +1,23 @@
+package functions
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+// GetKeys - fetch all access keys of a network
+func GetKeys(networkName string) *[]models.AccessKey {
+	return request[[]models.AccessKey](http.MethodGet, fmt.Sprintf("/api/networks/%s/keys", networkName), nil)
+}
+
+// CreateKey - create an access key
+func CreateKey(networkName string, key *models.AccessKey) *models.AccessKey {
+	return request[models.AccessKey](http.MethodPost, fmt.Sprintf("/api/networks/%s/keys", networkName), key)
+}
+
+// DeleteKey - delete an access key
+func DeleteKey(networkName, keyName string) {
+	request[string](http.MethodDelete, fmt.Sprintf("/api/networks/%s/keys/%s", networkName, keyName), nil)
+}

Some files were not shown because too many files changed in this diff