Add maxCallRecvMsgSize parameter and read it from the configuration
GRPC client max msg receive size
file. It allows to set if the incoming message is bigger than the
default 4K size.

Change-Id: I5b5113264066b84b5b055faf7d4bc02a3f11ec89
diff --git a/README.md b/README.md
index 81a9257..1c195ff 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,7 @@
   -8, --k8sconfig=FILE                  Location of Kubernetes config file [$KUBECONFIG]
       --kvstoretimeout=DURATION         timeout for calls to KV store [$KVSTORE_TIMEOUT]
   -o, --command-options=FILE            Location of command options default configuration file [$VOLTCTL_COMMAND_OPTIONS]
+  -m, --maxcallrecvmsgsize=SIZE         Max GRPC Client request size limit in bytes (eg: 4MB)
 
 Help Options:
   -h, --help                            Show this help message
@@ -226,3 +227,19 @@
 adapter-open-olt    github.com/opencord/voltha-openolt-adapter/internal/pkg/resourcemanager
 adapter-open-olt    main
 ```
+
+### Configuring the message size
+
+When you run VOLTHA with a high number of OLTs/ONUs is possible that the gRPC response exceeds the default 4MB in size.
+This will be addressed on the server side in a future release, but you have the option to exceed the allowed response size
+using the `-m, --maxcallrecvmsgsize=SIZE` option.
+
+The common error message when this happens is:
+```shell
+ERROR: RESOURCEEXHAUSTED: grpc: received message larger than max (4241141 vs. 4194304)
+```
+
+If that happens, retry the command passing the -m option, eg:
+```shell
+$ voltctl device list -m 8M
+```
\ No newline at end of file
diff --git a/go.sum b/go.sum
index 88fcc0d..2729387 100644
--- a/go.sum
+++ b/go.sum
@@ -12,6 +12,7 @@
 github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
diff --git a/internal/pkg/commands/command.go b/internal/pkg/commands/command.go
index 9b2afd0..d5a2785 100644
--- a/internal/pkg/commands/command.go
+++ b/internal/pkg/commands/command.go
@@ -19,6 +19,17 @@
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io/ioutil"
+	"log"
+	"net"
+	"os"
+	"path/filepath"
+	"reflect"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
 	"github.com/golang/protobuf/jsonpb"
 	"github.com/golang/protobuf/proto"
 	"github.com/opencord/voltctl/pkg/filter"
@@ -26,15 +37,6 @@
 	"github.com/opencord/voltctl/pkg/order"
 	"google.golang.org/grpc"
 	"gopkg.in/yaml.v2"
-	"io/ioutil"
-	"log"
-	"net"
-	"os"
-	"path/filepath"
-	"reflect"
-	"strconv"
-	"strings"
-	"time"
 )
 
 type OutputType uint8
@@ -55,11 +57,13 @@
 	defaultKvPort        = 2379
 	defaultKvTimeout     = time.Second * 5
 
-	defaultGrpcTimeout = time.Minute * 5
+	defaultGrpcTimeout            = time.Minute * 5
+	defaultGrpcMaxCallRecvMsgSize = "4MB"
 )
 
 type GrpcConfigSpec struct {
-	Timeout time.Duration `yaml:"timeout"`
+	Timeout            time.Duration `yaml:"timeout"`
+	MaxCallRecvMsgSize string        `yaml:"maxCallRecvMsgSize"`
 }
 
 type KvStoreConfigSpec struct {
@@ -109,7 +113,8 @@
 			UseTls: false,
 		},
 		Grpc: GrpcConfigSpec{
-			Timeout: defaultGrpcTimeout,
+			Timeout:            defaultGrpcTimeout,
+			MaxCallRecvMsgSize: defaultGrpcMaxCallRecvMsgSize,
 		},
 		KvStoreConfig: KvStoreConfigSpec{
 			Timeout: defaultKvTimeout,
@@ -125,16 +130,17 @@
 		KvStore string `short:"e" long:"kvstore" env:"KVSTORE" value-name:"SERVER:PORT" description:"IP/Host and port of KV store (etcd)"`
 
 		// nolint: staticcheck
-		Debug          bool   `short:"d" long:"debug" description:"Enable debug mode"`
-		Timeout        string `short:"t" long:"timeout" description:"API call timeout duration" value-name:"DURATION" default:""`
-		UseTLS         bool   `long:"tls" description:"Use TLS"`
-		CACert         string `long:"tlscacert" value-name:"CA_CERT_FILE" description:"Trust certs signed only by this CA"`
-		Cert           string `long:"tlscert" value-name:"CERT_FILE" description:"Path to TLS vertificate file"`
-		Key            string `long:"tlskey" value-name:"KEY_FILE" description:"Path to TLS key file"`
-		Verify         bool   `long:"tlsverify" description:"Use TLS and verify the remote"`
-		K8sConfig      string `short:"8" long:"k8sconfig" env:"KUBECONFIG" value-name:"FILE" default:"" description:"Location of Kubernetes config file"`
-		KvStoreTimeout string `long:"kvstoretimeout" env:"KVSTORE_TIMEOUT" value-name:"DURATION" default:"" description:"timeout for calls to KV store"`
-		CommandOptions string `short:"o" long:"command-options" env:"VOLTCTL_COMMAND_OPTIONS" value-name:"FILE" default:"" description:"Location of command options default configuration file"`
+		Debug              bool   `short:"d" long:"debug" description:"Enable debug mode"`
+		Timeout            string `short:"t" long:"timeout" description:"API call timeout duration" value-name:"DURATION" default:""`
+		UseTLS             bool   `long:"tls" description:"Use TLS"`
+		CACert             string `long:"tlscacert" value-name:"CA_CERT_FILE" description:"Trust certs signed only by this CA"`
+		Cert               string `long:"tlscert" value-name:"CERT_FILE" description:"Path to TLS vertificate file"`
+		Key                string `long:"tlskey" value-name:"KEY_FILE" description:"Path to TLS key file"`
+		Verify             bool   `long:"tlsverify" description:"Use TLS and verify the remote"`
+		K8sConfig          string `short:"8" long:"k8sconfig" env:"KUBECONFIG" value-name:"FILE" default:"" description:"Location of Kubernetes config file"`
+		KvStoreTimeout     string `long:"kvstoretimeout" env:"KVSTORE_TIMEOUT" value-name:"DURATION" default:"" description:"timeout for calls to KV store"`
+		CommandOptions     string `short:"o" long:"command-options" env:"VOLTCTL_COMMAND_OPTIONS" value-name:"FILE" default:"" description:"Location of command options default configuration file"`
+		MaxCallRecvMsgSize string `short:"m" long:"maxcallrecvmsgsize" description:"Max GRPC Client request size limit in bytes (eg: 4MB)" value-name:"SIZE" default:"4M"`
 	}
 
 	Debug = log.New(os.Stdout, "DEBUG: ", 0)
@@ -227,6 +233,36 @@
 	return defaultValue
 }
 
+var sizeParser = regexp.MustCompile(`^([0-9]+)([PETGMK]?)I?B?$`)
+
+func parseSize(size string) (uint64, error) {
+
+	parts := sizeParser.FindAllStringSubmatch(strings.ToUpper(size), -1)
+	if len(parts) == 0 {
+		return 0, fmt.Errorf("size: invalid size '%s'", size)
+	}
+	value, err := strconv.ParseUint(parts[0][1], 10, 64)
+	if err != nil {
+		return 0, fmt.Errorf("size: invalid size '%s'", size)
+	}
+	switch parts[0][2] {
+	case "E":
+		value = value * 1024 * 1024 * 1024 * 1024 * 1024 * 1024
+	case "P":
+		value = value * 1024 * 1024 * 1024 * 1024 * 1024
+	case "T":
+		value = value * 1024 * 1024 * 1024 * 1024
+	case "G":
+		value = value * 1024 * 1024 * 1024
+	case "M":
+		value = value * 1024 * 1024
+	case "K":
+		value = value * 1024
+	default:
+	}
+	return value, nil
+}
+
 func ProcessGlobalOptions() {
 	if len(GlobalOptions.Config) == 0 {
 		home, err := os.UserHomeDir()
@@ -298,6 +334,10 @@
 		GlobalConfig.Grpc.Timeout = timeout
 	}
 
+	if GlobalOptions.MaxCallRecvMsgSize != "" {
+		GlobalConfig.Grpc.MaxCallRecvMsgSize = GlobalOptions.MaxCallRecvMsgSize
+	}
+
 	// If a k8s cert/key were not specified, then attempt to read it from
 	// any $HOME/.kube/config if it exists
 	if len(GlobalOptions.K8sConfig) == 0 {
@@ -333,7 +373,14 @@
 
 func NewConnection() (*grpc.ClientConn, error) {
 	ProcessGlobalOptions()
-	return grpc.Dial(GlobalConfig.Server, grpc.WithInsecure())
+
+	// convert grpc.msgSize into bytes
+	n, err := parseSize(GlobalConfig.Grpc.MaxCallRecvMsgSize)
+	if err != nil {
+		Error.Fatalf("Cannot convert msgSize %s to bytes", GlobalConfig.Grpc.MaxCallRecvMsgSize)
+	}
+
+	return grpc.Dial(GlobalConfig.Server, grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(int(n))))
 }
 
 func ConvertJsonProtobufArray(data_in interface{}) (string, error) {
diff --git a/internal/pkg/commands/command_test.go b/internal/pkg/commands/command_test.go
index 8dd449b..68dd0a4 100644
--- a/internal/pkg/commands/command_test.go
+++ b/internal/pkg/commands/command_test.go
@@ -90,3 +90,24 @@
 		})
 	}
 }
+
+func TestParseSize(t *testing.T) {
+	var res uint64
+	var err error
+
+	res, err = parseSize("8M")
+	assert.Nil(t, err)
+	assert.Equal(t, uint64(8388608), res)
+
+	res, err = parseSize("8MB")
+	assert.Nil(t, err)
+	assert.Equal(t, uint64(8388608), res)
+
+	res, err = parseSize("8MiB")
+	assert.Nil(t, err)
+	assert.Equal(t, uint64(8388608), res)
+
+	res, err = parseSize("foobar")
+	assert.NotNil(t, err)
+	assert.Equal(t, uint64(0), res)
+}
diff --git a/voltctl.config b/voltctl.config
index f07a75b..857219a 100644
--- a/voltctl.config
+++ b/voltctl.config
@@ -9,5 +9,6 @@
   verify: ""
 grpc:
   timeout: 5m0s
+  maxCallRecvMsgSize: 4MB
 kvstoreconfig:
   timeout: 5s