blob: d2951a3dd8ff3450a6a753312a631395fcdde306 [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001/*
2 * Copyright 2019-present Ciena Corporation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package commands
17
18import (
19 "encoding/json"
Scott Baker9173ed82020-05-19 08:30:12 -070020 "errors"
Zack Williamse940c7a2019-08-21 14:25:39 -070021 "fmt"
serkant.uluderyad3daa902020-05-21 22:49:25 -070022 "io/ioutil"
23 "log"
24 "net"
25 "os"
26 "path/filepath"
27 "reflect"
28 "regexp"
29 "strconv"
30 "strings"
31 "time"
32
Scott Baker9173ed82020-05-19 08:30:12 -070033 "github.com/golang/protobuf/jsonpb"
34 "github.com/golang/protobuf/proto"
Scott Baker2b0ad652019-08-21 14:57:07 -070035 "github.com/opencord/voltctl/pkg/filter"
36 "github.com/opencord/voltctl/pkg/format"
37 "github.com/opencord/voltctl/pkg/order"
Zack Williamse940c7a2019-08-21 14:25:39 -070038 "google.golang.org/grpc"
39 "gopkg.in/yaml.v2"
Zack Williamse940c7a2019-08-21 14:25:39 -070040)
41
42type OutputType uint8
43
44const (
45 OUTPUT_TABLE OutputType = iota
46 OUTPUT_JSON
47 OUTPUT_YAML
divyadesai19009132020-03-04 12:58:08 +000048
49 defaultApiHost = "localhost"
50 defaultApiPort = 55555
51
52 defaultKafkaHost = "localhost"
53 defaultKafkaPort = 9092
54
55 supportedKvStoreType = "etcd"
56 defaultKvHost = "localhost"
57 defaultKvPort = 2379
58 defaultKvTimeout = time.Second * 5
59
serkant.uluderyad3daa902020-05-21 22:49:25 -070060 defaultGrpcTimeout = time.Minute * 5
61 defaultGrpcMaxCallRecvMsgSize = "4MB"
Zack Williamse940c7a2019-08-21 14:25:39 -070062)
63
64type GrpcConfigSpec struct {
serkant.uluderyad3daa902020-05-21 22:49:25 -070065 Timeout time.Duration `yaml:"timeout"`
66 MaxCallRecvMsgSize string `yaml:"maxCallRecvMsgSize"`
Zack Williamse940c7a2019-08-21 14:25:39 -070067}
68
divyadesai19009132020-03-04 12:58:08 +000069type KvStoreConfigSpec struct {
70 Timeout time.Duration `yaml:"timeout"`
71}
72
Zack Williamse940c7a2019-08-21 14:25:39 -070073type TlsConfigSpec struct {
74 UseTls bool `yaml:"useTls"`
75 CACert string `yaml:"caCert"`
76 Cert string `yaml:"cert"`
77 Key string `yaml:"key"`
78 Verify string `yaml:"verify"`
79}
80
81type GlobalConfigSpec struct {
divyadesai19009132020-03-04 12:58:08 +000082 Server string `yaml:"server"`
83 Kafka string `yaml:"kafka"`
84 KvStore string `yaml:"kvstore"`
85 Tls TlsConfigSpec `yaml:"tls"`
86 Grpc GrpcConfigSpec `yaml:"grpc"`
87 KvStoreConfig KvStoreConfigSpec `yaml:"kvstoreconfig"`
88 K8sConfig string `yaml:"-"`
Zack Williamse940c7a2019-08-21 14:25:39 -070089}
90
91var (
92 ParamNames = map[string]map[string]string{
93 "v1": {
94 "ID": "voltha.ID",
95 },
96 "v2": {
97 "ID": "common.ID",
98 },
kesavand12cd8eb2020-01-20 22:25:22 -050099 "v3": {
Dinesh Belwalkarc9aa6d82020-03-04 15:22:17 -0800100 "ID": "common.ID",
101 "port": "voltha.Port",
102 "ValueSpecifier": "common.ValueSpecifier",
kesavand12cd8eb2020-01-20 22:25:22 -0500103 },
Zack Williamse940c7a2019-08-21 14:25:39 -0700104 }
105
106 CharReplacer = strings.NewReplacer("\\t", "\t", "\\n", "\n")
107
108 GlobalConfig = GlobalConfigSpec{
Scott Baker80126ab2020-06-04 11:49:07 -0700109 Server: "localhost:55555",
110 Kafka: "",
111 KvStore: "localhost:2379",
Zack Williamse940c7a2019-08-21 14:25:39 -0700112 Tls: TlsConfigSpec{
113 UseTls: false,
114 },
115 Grpc: GrpcConfigSpec{
serkant.uluderyad3daa902020-05-21 22:49:25 -0700116 Timeout: defaultGrpcTimeout,
117 MaxCallRecvMsgSize: defaultGrpcMaxCallRecvMsgSize,
divyadesai19009132020-03-04 12:58:08 +0000118 },
119 KvStoreConfig: KvStoreConfigSpec{
120 Timeout: defaultKvTimeout,
Zack Williamse940c7a2019-08-21 14:25:39 -0700121 },
122 }
123
David Bainbridgea6722342019-10-24 23:55:53 +0000124 GlobalCommandOptions = make(map[string]map[string]string)
125
Zack Williamse940c7a2019-08-21 14:25:39 -0700126 GlobalOptions struct {
divyadesai19009132020-03-04 12:58:08 +0000127 Config string `short:"c" long:"config" env:"VOLTCONFIG" value-name:"FILE" default:"" description:"Location of client config file"`
128 Server string `short:"s" long:"server" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of VOLTHA"`
129 Kafka string `short:"k" long:"kafka" default:"" value-name:"SERVER:PORT" description:"IP/Host and port of Kafka"`
130 KvStore string `short:"e" long:"kvstore" env:"KVSTORE" value-name:"SERVER:PORT" description:"IP/Host and port of KV store (etcd)"`
131
David K. Bainbridge2b627612020-02-18 14:50:13 -0800132 // nolint: staticcheck
serkant.uluderyad3daa902020-05-21 22:49:25 -0700133 Debug bool `short:"d" long:"debug" description:"Enable debug mode"`
134 Timeout string `short:"t" long:"timeout" description:"API call timeout duration" value-name:"DURATION" default:""`
135 UseTLS bool `long:"tls" description:"Use TLS"`
136 CACert string `long:"tlscacert" value-name:"CA_CERT_FILE" description:"Trust certs signed only by this CA"`
137 Cert string `long:"tlscert" value-name:"CERT_FILE" description:"Path to TLS vertificate file"`
138 Key string `long:"tlskey" value-name:"KEY_FILE" description:"Path to TLS key file"`
139 Verify bool `long:"tlsverify" description:"Use TLS and verify the remote"`
140 K8sConfig string `short:"8" long:"k8sconfig" env:"KUBECONFIG" value-name:"FILE" default:"" description:"Location of Kubernetes config file"`
141 KvStoreTimeout string `long:"kvstoretimeout" env:"KVSTORE_TIMEOUT" value-name:"DURATION" default:"" description:"timeout for calls to KV store"`
142 CommandOptions string `short:"o" long:"command-options" env:"VOLTCTL_COMMAND_OPTIONS" value-name:"FILE" default:"" description:"Location of command options default configuration file"`
143 MaxCallRecvMsgSize string `short:"m" long:"maxcallrecvmsgsize" description:"Max GRPC Client request size limit in bytes (eg: 4MB)" value-name:"SIZE" default:"4M"`
Zack Williamse940c7a2019-08-21 14:25:39 -0700144 }
David Bainbridgea6722342019-10-24 23:55:53 +0000145
146 Debug = log.New(os.Stdout, "DEBUG: ", 0)
147 Info = log.New(os.Stdout, "INFO: ", 0)
148 Warn = log.New(os.Stderr, "WARN: ", 0)
149 Error = log.New(os.Stderr, "ERROR: ", 0)
Zack Williamse940c7a2019-08-21 14:25:39 -0700150)
151
152type OutputOptions struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800153 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
154 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
155 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700156 OutputAs string `short:"o" long:"outputas" default:"table" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
157 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
158}
159
Maninder045921e2020-09-29 16:46:02 +0530160type FlowIdOptions struct {
161 HexId bool `short:"x" long:"hex-id" description:"Output Ids in hex format"`
162}
163
Zack Williamse940c7a2019-08-21 14:25:39 -0700164type ListOutputOptions struct {
165 OutputOptions
166 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
167 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
168}
169
170type OutputOptionsJson struct {
David K. Bainbridge2b627612020-02-18 14:50:13 -0800171 Format string `long:"format" value-name:"FORMAT" default:"" description:"Format to use to output structured data"`
172 Quiet bool `short:"q" long:"quiet" description:"Output only the IDs of the objects"`
173 // nolint: staticcheck
Zack Williamse940c7a2019-08-21 14:25:39 -0700174 OutputAs string `short:"o" long:"outputas" default:"json" choice:"table" choice:"json" choice:"yaml" description:"Type of output to generate"`
175 NameLimit int `short:"l" long:"namelimit" default:"-1" value-name:"LIMIT" description:"Limit the depth (length) in the table column name"`
176}
177
178type ListOutputOptionsJson struct {
179 OutputOptionsJson
180 Filter string `short:"f" long:"filter" default:"" value-name:"FILTER" description:"Only display results that match filter"`
181 OrderBy string `short:"r" long:"orderby" default:"" value-name:"ORDER" description:"Specify the sort order of the results"`
182}
183
184func toOutputType(in string) OutputType {
185 switch in {
186 case "table":
187 fallthrough
188 default:
189 return OUTPUT_TABLE
190 case "json":
191 return OUTPUT_JSON
192 case "yaml":
193 return OUTPUT_YAML
194 }
195}
196
divyadesai19009132020-03-04 12:58:08 +0000197func splitEndpoint(ep, defaultHost string, defaultPort int) (string, int, error) {
198 port := defaultPort
199 host, sPort, err := net.SplitHostPort(ep)
200 if err != nil {
201 if addrErr, ok := err.(*net.AddrError); ok {
202 if addrErr.Err != "missing port in address" {
203 return "", 0, err
204 }
205 host = ep
206 } else {
207 return "", 0, err
208 }
209 } else if len(strings.TrimSpace(sPort)) > 0 {
210 val, err := strconv.Atoi(sPort)
211 if err != nil {
212 return "", 0, err
213 }
214 port = val
215 }
216 if len(strings.TrimSpace(host)) == 0 {
217 host = defaultHost
218 }
219 return strings.Trim(host, "]["), port, nil
220}
221
Zack Williamse940c7a2019-08-21 14:25:39 -0700222type CommandResult struct {
223 Format format.Format
224 Filter string
225 OrderBy string
226 OutputAs OutputType
227 NameLimit int
228 Data interface{}
229}
230
David Bainbridgea6722342019-10-24 23:55:53 +0000231func GetCommandOptionWithDefault(name, option, defaultValue string) string {
232 if cmd, ok := GlobalCommandOptions[name]; ok {
233 if val, ok := cmd[option]; ok {
234 return CharReplacer.Replace(val)
235 }
236 }
237 return defaultValue
238}
239
serkant.uluderyad3daa902020-05-21 22:49:25 -0700240var sizeParser = regexp.MustCompile(`^([0-9]+)([PETGMK]?)I?B?$`)
241
242func parseSize(size string) (uint64, error) {
243
244 parts := sizeParser.FindAllStringSubmatch(strings.ToUpper(size), -1)
245 if len(parts) == 0 {
246 return 0, fmt.Errorf("size: invalid size '%s'", size)
247 }
248 value, err := strconv.ParseUint(parts[0][1], 10, 64)
249 if err != nil {
250 return 0, fmt.Errorf("size: invalid size '%s'", size)
251 }
252 switch parts[0][2] {
253 case "E":
254 value = value * 1024 * 1024 * 1024 * 1024 * 1024 * 1024
255 case "P":
256 value = value * 1024 * 1024 * 1024 * 1024 * 1024
257 case "T":
258 value = value * 1024 * 1024 * 1024 * 1024
259 case "G":
260 value = value * 1024 * 1024 * 1024
261 case "M":
262 value = value * 1024 * 1024
263 case "K":
264 value = value * 1024
265 default:
266 }
267 return value, nil
268}
269
Zack Williamse940c7a2019-08-21 14:25:39 -0700270func ProcessGlobalOptions() {
271 if len(GlobalOptions.Config) == 0 {
272 home, err := os.UserHomeDir()
273 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000274 Warn.Printf("Unable to discover the user's home directory: %s", err)
Zack Williamse940c7a2019-08-21 14:25:39 -0700275 home = "~"
276 }
277 GlobalOptions.Config = filepath.Join(home, ".volt", "config")
278 }
279
David Bainbridgea6722342019-10-24 23:55:53 +0000280 if info, err := os.Stat(GlobalOptions.Config); err == nil && !info.IsDir() {
Zack Williamse940c7a2019-08-21 14:25:39 -0700281 configFile, err := ioutil.ReadFile(GlobalOptions.Config)
282 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000283 Error.Fatalf("Unable to read the configuration file '%s': %s",
284 GlobalOptions.Config, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700285 }
David Bainbridgea6722342019-10-24 23:55:53 +0000286 if err = yaml.Unmarshal(configFile, &GlobalConfig); err != nil {
287 Error.Fatalf("Unable to parse the configuration file '%s': %s",
288 GlobalOptions.Config, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700289 }
290 }
291
292 // Override from command line
293 if GlobalOptions.Server != "" {
294 GlobalConfig.Server = GlobalOptions.Server
295 }
divyadesai19009132020-03-04 12:58:08 +0000296 host, port, err := splitEndpoint(GlobalConfig.Server, defaultApiHost, defaultApiPort)
297 if err != nil {
298 Error.Fatalf("voltha API endport incorrectly specified '%s':%s",
299 GlobalConfig.Server, err)
300 }
301 GlobalConfig.Server = net.JoinHostPort(host, strconv.Itoa(port))
302
Scott Bakered4efab2020-01-13 19:12:25 -0800303 if GlobalOptions.Kafka != "" {
304 GlobalConfig.Kafka = GlobalOptions.Kafka
305 }
divyadesai19009132020-03-04 12:58:08 +0000306 host, port, err = splitEndpoint(GlobalConfig.Kafka, defaultKafkaHost, defaultKafkaPort)
307 if err != nil {
308 Error.Fatalf("Kafka endport incorrectly specified '%s':%s",
309 GlobalConfig.Kafka, err)
310 }
311 GlobalConfig.Kafka = net.JoinHostPort(host, strconv.Itoa(port))
312
313 if GlobalOptions.KvStore != "" {
314 GlobalConfig.KvStore = GlobalOptions.KvStore
315 }
316 host, port, err = splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort)
317 if err != nil {
318 Error.Fatalf("KV store endport incorrectly specified '%s':%s",
319 GlobalConfig.KvStore, err)
320 }
321 GlobalConfig.KvStore = net.JoinHostPort(host, strconv.Itoa(port))
322
divyadesai19009132020-03-04 12:58:08 +0000323 if GlobalOptions.KvStoreTimeout != "" {
324 timeout, err := time.ParseDuration(GlobalOptions.KvStoreTimeout)
325 if err != nil {
326 Error.Fatalf("Unable to parse specified KV strore timeout duration '%s': %s",
327 GlobalOptions.KvStoreTimeout, err.Error())
328 }
329 GlobalConfig.KvStoreConfig.Timeout = timeout
330 }
331
David K. Bainbridge402f8482020-02-26 17:14:46 -0800332 if GlobalOptions.Timeout != "" {
333 timeout, err := time.ParseDuration(GlobalOptions.Timeout)
334 if err != nil {
335 Error.Fatalf("Unable to parse specified timeout duration '%s': %s",
336 GlobalOptions.Timeout, err.Error())
337 }
338 GlobalConfig.Grpc.Timeout = timeout
339 }
340
serkant.uluderyad3daa902020-05-21 22:49:25 -0700341 if GlobalOptions.MaxCallRecvMsgSize != "" {
342 GlobalConfig.Grpc.MaxCallRecvMsgSize = GlobalOptions.MaxCallRecvMsgSize
343 }
344
Zack Williamse940c7a2019-08-21 14:25:39 -0700345 // If a k8s cert/key were not specified, then attempt to read it from
346 // any $HOME/.kube/config if it exists
347 if len(GlobalOptions.K8sConfig) == 0 {
348 home, err := os.UserHomeDir()
349 if err != nil {
David Bainbridgea6722342019-10-24 23:55:53 +0000350 Warn.Printf("Unable to discover the user's home directory: %s", err)
Zack Williamse940c7a2019-08-21 14:25:39 -0700351 home = "~"
352 }
353 GlobalOptions.K8sConfig = filepath.Join(home, ".kube", "config")
354 }
David Bainbridgea6722342019-10-24 23:55:53 +0000355
356 if len(GlobalOptions.CommandOptions) == 0 {
357 home, err := os.UserHomeDir()
358 if err != nil {
359 Warn.Printf("Unable to discover the user's home directory: %s", err)
360 home = "~"
361 }
362 GlobalOptions.CommandOptions = filepath.Join(home, ".volt", "command_options")
363 }
364
365 if info, err := os.Stat(GlobalOptions.CommandOptions); err == nil && !info.IsDir() {
366 optionsFile, err := ioutil.ReadFile(GlobalOptions.CommandOptions)
367 if err != nil {
368 Error.Fatalf("Unable to read command options configuration file '%s' : %s",
369 GlobalOptions.CommandOptions, err.Error())
370 }
371 if err = yaml.Unmarshal(optionsFile, &GlobalCommandOptions); err != nil {
372 Error.Fatalf("Unable to parse the command line options configuration file '%s': %s",
373 GlobalOptions.CommandOptions, err.Error())
374 }
375 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700376}
377
378func NewConnection() (*grpc.ClientConn, error) {
379 ProcessGlobalOptions()
serkant.uluderyad3daa902020-05-21 22:49:25 -0700380
381 // convert grpc.msgSize into bytes
382 n, err := parseSize(GlobalConfig.Grpc.MaxCallRecvMsgSize)
383 if err != nil {
384 Error.Fatalf("Cannot convert msgSize %s to bytes", GlobalConfig.Grpc.MaxCallRecvMsgSize)
385 }
386
387 return grpc.Dial(GlobalConfig.Server, grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(int(n))))
Zack Williamse940c7a2019-08-21 14:25:39 -0700388}
389
Scott Baker9173ed82020-05-19 08:30:12 -0700390func ConvertJsonProtobufArray(data_in interface{}) (string, error) {
391 result := ""
392
393 slice := reflect.ValueOf(data_in)
394 if slice.Kind() != reflect.Slice {
395 return "", errors.New("Not a slice")
396 }
397
398 result = result + "["
399
400 marshaler := jsonpb.Marshaler{EmitDefaults: true}
401 for i := 0; i < slice.Len(); i++ {
402 item := slice.Index(i).Interface()
403 protoMessage, okay := item.(proto.Message)
404 if !okay {
405 return "", errors.New("Failed to convert item to a proto.Message")
406 }
407 asJson, err := marshaler.MarshalToString(protoMessage)
408 if err != nil {
409 return "", fmt.Errorf("Failed to marshal the json: %s", err)
410 }
411
412 result = result + asJson
413
414 if i < slice.Len()-1 {
415 result = result + ","
416 }
417 }
418
419 result = result + "]"
420
421 return result, nil
422}
423
Zack Williamse940c7a2019-08-21 14:25:39 -0700424func GenerateOutput(result *CommandResult) {
425 if result != nil && result.Data != nil {
426 data := result.Data
427 if result.Filter != "" {
428 f, err := filter.Parse(result.Filter)
429 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000430 Error.Fatalf("Unable to parse specified output filter '%s': %s", result.Filter, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700431 }
432 data, err = f.Process(data)
433 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000434 Error.Fatalf("Unexpected error while filtering command results: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700435 }
436 }
437 if result.OrderBy != "" {
438 s, err := order.Parse(result.OrderBy)
439 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000440 Error.Fatalf("Unable to parse specified sort specification '%s': %s", result.OrderBy, err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700441 }
442 data, err = s.Process(data)
443 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000444 Error.Fatalf("Unexpected error while sorting command result: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700445 }
446 }
447 if result.OutputAs == OUTPUT_TABLE {
448 tableFormat := format.Format(result.Format)
David Bainbridge12f036f2019-10-15 22:09:04 +0000449 if err := tableFormat.Execute(os.Stdout, true, result.NameLimit, data); err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000450 Error.Fatalf("Unexpected error while attempting to format results as table : %s", err.Error())
David Bainbridge12f036f2019-10-15 22:09:04 +0000451 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700452 } else if result.OutputAs == OUTPUT_JSON {
Scott Baker9173ed82020-05-19 08:30:12 -0700453 // first try to convert it as an array of protobufs
454 asJson, err := ConvertJsonProtobufArray(data)
Zack Williamse940c7a2019-08-21 14:25:39 -0700455 if err != nil {
Scott Baker9173ed82020-05-19 08:30:12 -0700456 // if that fails, then just do a standard json conversion
457 asJsonB, err := json.Marshal(&data)
458 if err != nil {
459 Error.Fatalf("Unexpected error while processing command results to JSON: %s", err.Error())
460 }
461 asJson = string(asJsonB)
Zack Williamse940c7a2019-08-21 14:25:39 -0700462 }
463 fmt.Printf("%s", asJson)
464 } else if result.OutputAs == OUTPUT_YAML {
465 asYaml, err := yaml.Marshal(&data)
466 if err != nil {
David Bainbridge0f758d42019-10-26 05:17:48 +0000467 Error.Fatalf("Unexpected error while processing command results to YAML: %s", err.Error())
Zack Williamse940c7a2019-08-21 14:25:39 -0700468 }
469 fmt.Printf("%s", asYaml)
470 }
471 }
472}